# 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. Published 2026-08-12 · Updated 2026-08-16 · HTML version: https://invoket.com/blog/the-16-kb-ceiling-on-a-402-challenge --- 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](https://docs.x402.org/core-concepts/http-402) 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](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) 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: 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`](https://api.invoket.com/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](/docs/for-agents) and mapped in [How does an agent find a paid API with no human in the loop?](/blog/machine-readable-discovery-x402-bazaar-mcp). ## 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: ```sh 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`](/docs/api/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](/docs/quickstart); the accepted assets and networks are in [Payments and rails](/docs/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](https://api.invoket.com/catalog), never from prose like this. ## Sources - x402 HTTP transport binding, base64 headers — [docs.x402.org: HTTP 402](https://docs.x402.org/core-concepts/http-402). - x402 v2 specification (transport-agnostic core, `extensions`) — [coinbase/x402 specs](https://github.com/coinbase/x402/blob/main/specs/x402-specification-v2.md). - Client payment wrapper reading the challenge — [`@x402/fetch`](https://www.npmjs.com/package/@x402/fetch). - Node.js header parsing limit and override — [`http.maxHeaderSize`](https://nodejs.org/api/http.html#httpmaxheadersize), [`--max-http-header-size`](https://nodejs.org/api/cli.html#--max-http-header-sizesize). - nginx upstream header buffer — [`proxy_buffer_size`](https://nginx.org/en/docs/http/ngx_http_proxy_module.html). - CDN header limits — [Cloudflare: increased HTTP header size limit to 128 KB](https://developers.cloudflare.com/changelog/post/2025-10-16-header-limit-increase/). - Measurements: Invoket gateway, 67 paid endpoints, 12 August 2026; Node v20.19.2.