The 16 KB ceiling on a 402 challenge
In x402, discovery rides inside an HTTP response header — so it has a budget. Where the 16 KB ceiling comes from, what to cut first, and what to never cut.
By Matthias Begot · · Updated
In x402, the offer travels with the refusal: a paid endpoint answers 402 and
describes its own terms in the PAYMENT-REQUIRED response header, base64-encoded.
That design is what makes an endpoint self-describing — and it puts your discovery
surface inside an HTTP header, which has a budget that a response body does not.
The practical ceiling for a seller is 16 KB for the entire 402 response header
block, and the reason is not your server: it is the default HTTP parser limit in
the runtime your buyer is using. Above it, the agent never sees your challenge. It
sees a parse error.
This is a note from the seller side of the protocol: where the limit comes from, how it fails, what to cut first, and the one thing that must never be cut to make room.
Why the challenge is in a header at all
The x402 v2 HTTP binding puts each leg of the exchange in a header carrying
base64-encoded JSON: PAYMENT-REQUIRED from the server, PAYMENT-SIGNATURE from
the client on the replay, PAYMENT-RESPONSE for the settlement outcome. The
x402 documentation is explicit that
“all headers contain valid Base64-encoded JSON strings,” so that the wire format
survives every HTTP implementation in the path.
The challenge is not a price tag. It is the whole offer:
resource— the URL, a description, the media type it returns.accepts[]— one entry per rail, each with its scheme, network, asset, amount, recipient and timeout.extensions— the optional, non-core part, where a seller puts the input schema an agent needs to build a valid request.
The first two are small and fixed. The third is where a header goes bad, because it is the only part that grows with how well you document yourself.
Two multipliers apply before you count anything. Base64 costs about a third on top
of the JSON: on Invoket’s /company/report, a 2,163-byte challenge object becomes a
2,885-byte header value. And the ceiling applies to the whole header block, so
your security headers, Content-Type and Date come out of the same budget.
Where the ceiling comes from
Three layers can refuse an oversized response header. They are not equally dangerous, and the tightest one is the one you do not control.
1. The buyer’s runtime — 16 KiB, and not yours to configure
Most x402 clients today are Node processes: the @x402/fetch wrapper, MCP servers,
agent runtimes. Node caps HTTP header parsing at 16 KiB by default (raised from
8 KiB in v13.13.0), configurable only by whoever launches the process, via
--max-http-header-size. It is a parser limit, so it applies to responses the
client reads, not just requests a server receives.
Measured on Node v20.19.2 (http.maxHeaderSize = 16384), fetching a 402 whose
PAYMENT-REQUIRED header is padded to a given size:
| Header size | node:http | fetch (undici) |
|---|---|---|
| 8,000 B | 402 | 402 |
| 15,000 B | 402 | 402 |
| 16,000 B | 402 | 402 |
| 17,000 B | HPE_HEADER_OVERFLOW | UND_ERR_HEADERS_OVERFLOW |
| 20,900 B | HPE_HEADER_OVERFLOW | UND_ERR_HEADERS_OVERFLOW |
Note what the failure is not. It is not a 4xx, not a 5xx, not a status code at
all — the response never becomes a response. A client that catches the error sees a
transport failure from your domain, indistinguishable from an outage. This limit
lives on the buyer’s machine, and you cannot raise it from your side. It is the real
ceiling.
2. Your own reverse proxy — 4 KB or 8 KB by default
Long before the agent is involved, your own infrastructure can reject you. nginx
reads the upstream response header into a single buffer sized by proxy_buffer_size,
whose default is one memory page — 4k or 8k depending on the platform, and
the nginx documentation
states plainly that “if it exceeds the buffer size, the response is considered
invalid.” In practice that surfaces as upstream sent too big header in the error
log and a 502 to the caller.
This is how it actually broke for us: an enriched challenge on a composite endpoint — input schema plus field documentation plus a full output example — reached roughly 20.9 KB and started returning 502 at the proxy, before a single agent had a chance to parse anything. The fix is one line of proxy configuration; the lesson is that the first thing a fat challenge breaks is your own edge, and it breaks it as a 502 that looks like your server is down.
3. The CDN — no longer the binding constraint
Cloudflare raised its HTTP header limit to 128 KB for both requests and responses in October 2025, up from a previous 32 KB total with 16 KB per individual header. If you sit behind a modern CDN, that layer will not be what stops you. Which is exactly why it is worth being explicit about layer 1: the constraint that matters is the one running on someone else’s laptop, not the one on your invoice.
The body is free — use it
The asymmetry to exploit: a 402 has a body, and the body has no header budget.
Invoket sends both, deliberately, and they are not the same document.
For /company/report on 12 August 2026:
| Surface | Size | Carries |
|---|---|---|
PAYMENT-REQUIRED header | 2,903 B | resource, accepts[], extensions.bazaar.schema |
402 response body | 14,667 B | the same, plus extensions.bazaar.info |
The split is by role, not by importance. The header carries the minimum viable
challenge: what an agent needs to choose a rail, sign, and construct a valid
request — the JSON Schema included, since a schema is machine-usable. The body
carries the same thing plus info: human-readable field descriptions and a complete
example of the output. That example is by far the largest object in the contract and
the least necessary to make the call, which makes it the first thing to cut from the
header.
Order of eviction, when a challenge is over budget:
- The output example. Big, prose-adjacent, useless for constructing a request.
- Field descriptions. Keep the types and the
requiredlist; move the prose out. - Long resource descriptions. One sentence that says what the endpoint returns.
- Never the rails, never the schema’s required fields, never the header itself.
And the full contract is not lost — it lives where size is cheap. Invoket’s
/catalog is 646 KB of exactly this material,
served as a body, and it is the source of truth for terms; /openapi.json and
/.well-known/x402 are similar in scale. A header is a summons, not a manual. The
discovery surfaces are described in For agents and mapped in
How does an agent find a paid API with no human in the loop?.
What you must never cut
The tempting saving is the header itself: if the body already carries the challenge,
why send both? Because some clients and directories read only the header. We
removed PAYMENT-REQUIRED once, in July 2026, and the endpoints stayed up, kept
answering 402, kept serving a perfectly correct body — and quietly became
unquotable for anything that looked at the header first. There is no error to page
you on that. A body-only 402 is not a smaller challenge; for part of the market it
is no challenge at all.
The general rule for a seller: fit the header, do not remove it. The v2 core specification is transport-agnostic and defers HTTP details to the transport binding, so the header is where the interoperable contract actually lives on HTTP. Treat it as a required field with a size limit, not as an optional duplicate of the body.
Measure it per endpoint, not once
A header budget is not a property of your gateway; it is a property of each endpoint, because it grows with the size of that endpoint’s input schema — not with its price, its latency, or how much data it returns. The endpoint with the richest request body has the biggest challenge.
One command gives you the number:
curl -s -o /dev/null -w '%{http_code} %{size_header}\n' \
https://api.invoket.com/company/report?siren=662042449
Run across all 67 paid endpoints on 12 August 2026, the entire 402 header block
lands between 2,731 B and 9,136 B, averaging about 4.3 KB. The largest is
/invoice/generate at 9,136 B, whose challenge has to
describe a full EN 16931 invoice as input; among the smallest are the IBAN batch
routes, whose input is a list of one repeated shape. Even the largest sits a factor
of 1.8 below the 16 KB ceiling, and that headroom is the point: the check runs when
an endpoint ships, not after a buyer reports that your domain is broken.
A workable seller checklist:
- Measure
size_headeron every new or modified endpoint; treat 16 KB as the hard ceiling and something well under it as the target. - Budget in JSON terms at roughly three quarters of the header budget, for base64.
- Keep the output example and the prose in the body and in
/catalog, never in the header. - Verify the proxy in front of your origin buffers more than one memory page.
- Assert the
PAYMENT-REQUIREDheader exists in your smoke tests, not just the402status.
Why this is a protocol property, not an implementation detail
Putting discovery in the refusal is what makes x402 work without a registration step: an agent that has never heard of you learns your terms from the very response that denies it service. The cost of that elegance is that your contract must survive a transport designed for small metadata, on a parser you do not own. It is the same trade every challenge-response protocol makes, and it has the same discipline: keep the challenge minimal and machine-readable, and put the documentation one hop away.
The rest of the loop — 402, sign, replay — is unchanged and runnable in the
Quickstart; the accepted assets and networks are in
Payments and rails. As always, the amounts, rails and
recipient in any challenge are live authority data: they come from the gateway and
the catalog, never from prose like this.
Sources
- x402 HTTP transport binding, base64 headers — docs.x402.org: HTTP 402.
- x402 v2 specification (transport-agnostic core,
extensions) — coinbase/x402 specs. - Client payment wrapper reading the challenge —
@x402/fetch. - Node.js header parsing limit and override —
http.maxHeaderSize,--max-http-header-size. - nginx upstream header buffer —
proxy_buffer_size. - CDN header limits — Cloudflare: increased HTTP header size limit to 128 KB.
- Measurements: Invoket gateway, 67 paid endpoints, 12 August 2026; Node v20.19.2.