RELAY_PAYLOAD_QUEUE_BYTES, 1/16 of GOMEMLIMIT by default; see Configuration), and when it is full new captures are dropped and counted in relay_records_lost_total{kind="payload"}. Requests refused before reaching an upstream keep their captures in a small queue of their own (RELAY_PAYLOAD_REFUSED_QUEUE_BYTES, 2 MiB), written after the served ones and counted as kind="payload_refused" when dropped.
Enabling capture
1
Turn on the payload-logging section
PUT /api/settings/payload-logging with enabled: true and a backend. While enabled is false nothing is captured, whatever the policies say.2
Opt in on the policy
Set
payloadLoggingEnabled: true on the policy that governs the traffic. The policy decides, whatever the relay key’s own payloadLoggingEnabled says. Only a relay key that no policy governs opts in with its own flag.enabled, backend, maxBytes, retentionDays and every backend block are required, including the blocks not in use. To change one field, read the section with GET /api/settings/payload-logging and send its value back with the change. Changes apply without a restart. Only an admin may change this section.
What is never captured
- Requests whose governing policy does not have
payloadLoggingEnabled, and any request while the section is off. - Anonymous proxy traffic, which has no relay key.
- Batch items.
- Usage events themselves:
/api/usage/*and/api/logsnever carry bodies, only the detail endpoint below does.
Backends
File
JSONL on local disk atfile.path. It is never pruned, each pod writes and reads its own file, and the read path scans the whole file. Use it for a single node or a trial only.
S3
One JSON object per request at<prefix>/<YYYY>/<MM>/<DD>/<request_id>.json in an S3-compatible bucket (AWS S3, MinIO and others). Expire objects with a bucket lifecycle rule; Relay does not delete them.
The credentials are references limited to this section: an env reference must name a variable starting with
RELAY_PAYLOAD_S3_ (for example {"kind": "env", "env": "RELAY_PAYLOAD_S3_ACCESS_KEY"}), and a stored reference must use an id starting with payload-logging:. A stored section that breaks these rules is read as the default (capture off, file backend) and logged at startup; rename the variables and write the section again.
ClickHouse
Bodies are stored in ClickHouse on the connection usage uses (RELAY_CH_DSN), buffered in a local write-ahead log first (clickhouse.walDir, default relay-payload-wal). retentionDays becomes the table’s TTL and is applied to the existing table whenever the section changes, so shortening it deletes older bodies. This is the only backend that supports erasure.
Deduplication
Agent clients resend the whole conversation on every turn, so most of each request body repeats earlier ones.clickhouse.dedup: true (off by default) stores each distinct message, tool list, and large field such as a system prompt once, and each request as the rest of its body plus references to them.
- Bodies read back byte for byte. A body that is not a JSON object, including a truncated one, is stored whole, so pair dedup with
maxBytes: 0. - Pieces are shared only between requests of the same project.
- Each request also stores the SHA-256 of its body as received; a body that does not rebuild to that digest is refused with an error rather than returned.
- Response bodies are not deduplicated.
- Rows written before switching either way stay readable.
Reading captured payloads
The bodies come back on the request log’s detail endpoint, underpayload:
payload is absent when the request was not captured or its body has expired. The same read scope applies as for usage: a caller who may not see the event gets 404, and a body is joined only when it was captured for the same project, principal and relay key as the event.
Erasing captured bodies
An admin can delete the bodies captured for a project, for a principal (a user or service account), or for one principal within one project:projectId and principalId is required. The response, {"requests": N}, counts the requests whose bodies were deleted. The call is recorded in the audit log.
- The call returns once ClickHouse has rewritten the data that held them, so they can no longer be read; ClickHouse deletes the replaced files from disk after
old_parts_lifetime(8 minutes by default). - Usage events (tokens, cost, timing, ids) are kept.
- Bodies a relay has captured but not yet delivered to ClickHouse are not affected; repeat the call after the traffic has stopped.
- With
clickhouse.dedup, a request of the same project that another relay stores while the erase runs can be left pointing at a deleted piece. Reading it fails with an error, never returns different bytes, until a later request of that project carries the piece again. - Only the
clickhousebackend supports erasure, and only for the backendpayload-loggingcurrently names;fileands3answer501.