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 sizenode:httpfetch (undici)
8,000 B402402
15,000 B402402
16,000 B402402
17,000 BHPE_HEADER_OVERFLOWUND_ERR_HEADERS_OVERFLOW
20,900 BHPE_HEADER_OVERFLOWUND_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:

SurfaceSizeCarries
PAYMENT-REQUIRED header2,903 Bresource, accepts[], extensions.bazaar.schema
402 response body14,667 Bthe 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:

  1. The output example. Big, prose-adjacent, useless for constructing a request.
  2. Field descriptions. Keep the types and the required list; move the prose out.
  3. Long resource descriptions. One sentence that says what the endpoint returns.
  4. 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_header on 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-REQUIRED header exists in your smoke tests, not just the 402 status.

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