Cost receipts

The X-Cost response header: exact format, how to enable it, and why receipts are permission-gated.

#Cost receipts

A cost receipt is a per-request breakdown of everything the request consumed, returned in the X-Cost response header. Where the Performance dashboard shows trends, a receipt shows one request in full — the fastest way to answer "what did that call just cost?"

#Format

X-Cost: dur=142;d1q=9;rr=1420;rw=2;rret=27;d1ms=38;kvr=2;kvw=0;kvd=0;kvl=0;do=1;r2=0;sub=0
X-Cost-Slowest: rows=1380;ms=22;sql=SELECT * FROM records WHERE ...

X-Cost is a semicolon-separated list of key=value pairs, always in this order:

Key Meaning
dur Total request duration, milliseconds (wall clock, not CPU time)
d1q D1 queries executed
rr D1 rows read across all queries
rw D1 rows written across all queries
rret Rows actually returned to your code. rr ÷ rret is the read amplification
d1ms Time spent in D1, milliseconds
kvr KV reads
kvw KV writes
kvd KV deletes
kvl KV list operations
do Durable Object calls
r2 R2 operations
sub Outbound subrequests (webhooks, external APIs)

X-Cost-Slowest describes the request's single most expensive D1 query, ranked by rows read:

Key Meaning
rows Rows this query read
ms This query's duration, milliseconds
sql The SQL text, truncated to 160 characters. Bound parameter values are never included

#How to enable receipts

In development (ENVIRONMENT=development), receipts are always on — every response carries X-Cost.

In production, a response includes a receipt only when all three conditions hold:

  1. The request sends the header X-Cost-Debug: 1.
  2. The caller is authenticated and holds the system/cost-receipts:read permission. Admins have it by default; it can be granted to any role through standard role permissions.
  3. The analytics.cost_receipts setting is on (it's the global kill switch, on by default).

Anonymous requests never receive a receipt, regardless of settings.

#Example

$ curl -i -H "X-Cost-Debug: 1" -H "Authorization: Bearer sk-your-api-key" "https://your-api.example.com/api/v1/collections/products/records?limit=25"
HTTP/2 200
content-type: application/json
X-Cost: dur=96;d1q=7;rr=612;rw=0;rret=25;d1ms=24;kvr=1;kvw=0;kvd=0;kvl=0;do=1;r2=0;sub=0
X-Cost-Slowest: rows=540;ms=11;sql=SELECT * FROM c_products WHERE json_extract(...)

Reading this receipt: the request ran 7 D1 queries reading 612 rows to return 25 records — roughly 24× read amplification. The slowest query read 540 of those rows, and its SQL shows a json_extract filter, which can't use an index. The 1 KV read is the session cache; the 1 DO call is the rate limiter — both part of the normal per-request baseline.

Tip

Both headers are exposed via CORS, and X-Cost-Debug is an allowed request header — so you can read receipts from browser DevTools or a fetch() call, not only from curl.

#Why receipts are permission-gated

A receipt reveals internals: how many queries an endpoint runs, the shape of its slowest SQL, which tables it touches. That's exactly what you want when tuning your own instance, and exactly what you don't want to hand to an anonymous visitor probing your API.

Two design rules keep receipts safe to enable:

  • A receipt only ever describes your own request. There is no way to fetch a receipt for someone else's traffic — the header is computed for, and attached to, the response you triggered. For historical traffic, the dashboard shows recorded numbers without SQL text.
  • SQL is truncated and parameters are never captured. Even with the permission, a receipt can't leak the values in a query — only its shape, cut to 160 characters.

Because gating uses the standard permission system, you can grant system/cost-receipts:read to a developer role on a production instance without making anyone an admin.