inputs you hand it
verbatim; if a field is sensitive, redact it yourself before the call (see below).
References, not bytes
keeps records that a generation happened and where its outputs live — never the media itself. You pass outputs as URLs and metadata:capture() is coerced to a JSON-safe structure before it is
buffered. Raw binary never survives that step: a bytes/bytearray/memoryview
value is replaced with a length stub — {"__bytes__": true, "length": 41523} —
so even if you accidentally pass an image body, only its size is recorded.
This holds for inputs and outputs alike. If a provider hands you an inline
base64 image and you pass it through, keeps stores its length, not its content.
The asset bytes stay where the provider put them.
- Strings truncate at 8 KiB.
- An
inputsobject over 64 KiB collapses to a hashed truncation stub ({"__truncated__": true, "sha256": …, "length": …}) rather than shipping in full — the hash keeps equality analytics working. - A
propertiesbag over 8 KiB has its values trimmed to fit — keys are never replaced with a stub, so your group-by dimensions stay clean. outputscap at 50 references and 64 KiB total; over-cap lists keep the first references that fit plus one trailing{"__dropped__": n}marker recording how many were cut.
truncated array
naming each field the SDK touched, and the server stores it — so you can
always tell exactly which fields were cut, without inspecting payloads.
Typical events are well under 2 KiB.
Prompts are captured verbatim
keeps does not redact. Whatever you pass ininputs (and outputs) is
recorded as-is — the point of keeps is maximum fidelity on what your app
generated. There is no redaction switch to forget to turn on, and no field-name
heuristic that might silently miss a prompt hiding under a custom key.
If a specific field is sensitive, redact it yourself before the call. Because
you control the value, you decide exactly what is recorded — and you can keep a
stable hash so equality still holds server-side (group by prompt without ever
storing the text):
properties tags are your group-by dimensions, not payloads, and are recorded
as-is like everything else. If a tag value is sensitive (for example a raw
end-user identifier), hash it before you attach it:
Properties fold per key on the backend: each event updates only the keys it
carries (latest value per key by
ts wins), so keys you omit on a later call are
left intact, not dropped. For safety this means: once you set a key to a hashed
value, don’t later send that same key with the raw value — a newer event would
overwrite the hash for that key.Tenancy is derived server-side
The client never tells the backend which project the data belongs to. There is noproject_id parameter on any keeps call, and the SDK never sends one.
project_id is resolved server-side from the API key on the
Authorization: Bearer header of every batch. A client cannot spoof tenancy by
forging a field, and one project’s key can never write into — or read from —
another project’s data. Your API key scopes everything you send and everything
you can query.
If the key is missing or empty (and the SDK is not explicitly disabled), keeps
logs a single warning and disables itself rather than sending unauthenticated
data:
Never in the request path
keeps cannot affect the latency or availability of your provider calls, because it is never between you and your provider. There is no proxy mode — by design and forever. Your traffic to fal / Replicate / OpenAI flows exactly as it did before: same endpoints, same credentials, same TLS, same latency. You call keeps yourself, around your own code, and it reports out-of-band:capture() is a non-blocking, O(1) append to an in-memory buffer; it returns
immediately and a background daemon thread delivers events later. If keeps ingest
is slow, down, or returning errors, your provider call has already returned —
nothing in keeps can stall it. There are no sockets held open against your
provider, no agents, and no sidecars.
Fail-open, everywhere
Failure isolation is the SDK’s central design constraint. Every public call is fail-open: it can lose its own data, but it can never raise into, block, or alter your application.| Failure | What happens to your app |
|---|---|
| keeps ingest is down / slow / erroring | Nothing. Events retry briefly on a background thread, then drop and count. Your code already returned. |
| Buffer fills (traffic spike, long outage) | Memory is bounded (max_buffer, default 10,000). The newest event is dropped and counted; nothing blocks, nothing grows unbounded. |
| A bug inside keeps itself | Caught and logged at debug level. The exception never reaches your code. |
| Process forks (gunicorn prefork, etc.) | The child starts a clean buffer — no double sends, no inherited locks. |
| Misconfiguration (no API key) | SDK disables itself with one log line. Your app runs normally. |
| Kill switch | KEEPS_DISABLED=1 (or disabled=True) makes every call a no-op. |
POST of JSON (gzipped once a batch crosses ~1 KB) to a single endpoint,
retried up to three times with jittered exponential backoff, honoring
Retry-After on 429s, and never retrying any other 4xx (a rejected batch is
dropped, not re-sent).
You can watch the counters at any time:
dropped_overflow or dropped_send tells you keeps shed load — and
that your app was never affected while it did.
Because delivery is asynchronous, in short-lived or serverless workers the
process may be frozen or killed before the 1-second flush tick. Call
keeps.flush() before the unit of work returns so buffered events are delivered.
See Reliability for the full pattern.Verify it yourself
You never have to take the above on faith.Point keeps at your own collector
Set
KEEPS_INGEST_URL (or pass endpoint=) to an endpoint you control and
inspect every byte the SDK emits.init() appends /v1/events to the endpoint if you don’t include a path.
The default endpoint is an RFC 2606 .invalid host that can never
resolve to a third party — until you set KEEPS_INGEST_URL (or pass
endpoint=), nothing is sent anywhere.Confirm what leaves your process
Point
KEEPS_INGEST_URL at a collector you control, capture a known event,
and inspect the payload — you will see your inputs recorded verbatim (keeps
does not redact) and any bytes value reduced to a {__bytes__, length}
stub. Redact sensitive fields yourself before the call.Trivially removable
There are no decorators, no monkey-patching, and no auto-instrumentation. keeps only runs where you explicitly call it, so removing it is mechanical: delete yourkeeps.init(...) and keeps.capture(...) lines, and the SDK is gone. Nothing
about your provider calls changes, because keeps was never in them.
The single fastest off switch — no code change at all — is the environment:
Related
Reliability
Buffering, flushing, overflow, and the serverless flush pattern in depth.
Recording provider calls
How to instrument fal / Replicate / OpenAI calls with
capture().Core concepts
Events, assets, correlation by
asset_id, and properties.API reference
Every option on
init, capture, context, flush, and stats.