skip to content

Why does RFC 6648 deprecate the X- prefix on new HTTP header field names, and how would you name and design a custom header today?

level: middleimportance: should knowfreq 42%

answer

  1. X- experiment that succeeded = permanent wart
  2. Forwarded vs X-Forwarded-For = the cautionary tale
  3. pick the final name now, vendor-scope it
  4. names case-insensitive, lowercase in HTTP/2 and 3
  5. no secrets, no PII, keep it small

basics

~20 s

The X- prefix was meant to mark experiments, but successful ones get standardised and then you are stuck with either a permanent X- name or two names for one thing. RFC 6648 says pick the real name immediately, ideally vendor-scoped, and never treat X- as meaningful.

solid answer

~50 s

**RFC 6648** deprecates `X-` for new parameters, including HTTP field names. The reasoning: the prefix was supposed to signal "experimental/unstandardised", but useful experiments get adopted, and then you face a bad choice — keep an `X-` name forever (`X-Forwarded-For`) or standardise a second name and support both (`Forwarded` alongside `X-Forwarded-For`) essentially forever. The prefix also implies a distinction implementations do not actually honour: nothing treats `X-` fields differently. Today: **choose the permanent name at the start**, scope it to your organisation or product to avoid collision (`Acme-Tenant-Id` rather than `X-Tenant-Id`), keep it short, use hyphenated ASCII tokens, and remember names are **case-insensitive** and go over the wire lowercased in HTTP/2 and HTTP/3. Design-wise: one value per field with a defined syntax, ASCII only (non-ASCII needs encoding), no secrets or PII, keep it small, and check whether an existing standard field or the URI/body already expresses it. And verify intermediaries forward it — unknown fields are frequently dropped unless allowlisted.

code

http · 5 lines
http
X-Acme-Tenant: acme-eu
X-Acme-Features: beta,rollout

Acme-Tenant: "acme-eu"
Acme-Features: beta, rollout;stage=2

go deeper

for a junior

Know that X- is deprecated for new headers by RFC 6648 and that header names are case-insensitive.

for a middle

Explain the reasoning through the Forwarded versus X-Forwarded-For story, and give concrete naming and value rules including size and ASCII limits.

for a senior

Add the operational path: proxy allowlists and stripping, CORS preflight and Access-Control-Expose-Headers, comma-combining of repeated fields, and Structured Fields for new values.

for a principal

Own the namespace policy — vendor scoping, when data belongs in a header versus the body or URI, and the permanent cost of every field the platform ships.

## What the X- convention was For decades, the convention across email, HTTP and other text protocols was that unstandardised parameters get an `X-` prefix: `X-Forwarded-For`, `X-Requested-With`, `X-Powered-By`. The idea was that the prefix warns readers "this is not a standard field". ## Why RFC 6648 killed it RFC 6648 (2012, Best Current Practice) deprecates the convention for new parameters in **all** application protocols, HTTP included. The arguments: 1. **Successful experiments outlive the label.** `X-Forwarded-For` is universally deployed. Standardising it meant either blessing an `X-` name into a standard (embarrassing and confusing) or minting `Forwarded` (RFC 7239) and living with two fields for the same information for the rest of time. Every migration like this doubles parsing work permanently. 2. **The distinction is not honoured.** No proxy, cache or client treats `X-` fields specially; there is no separate namespace, no different processing, no protection from collision. The prefix conveys nothing operationally. 3. **Renaming is impossible in practice.** If you name it `X-Tenant-Id` and later want `Tenant-Id`, every client, gateway rule, WAF rule, log pipeline and dashboard must change, and you support both during a window that never really ends. The RFC's guidance: pick the name you intend to keep, immediately. ## Naming a custom field today - **Scope it.** The field-name space is global and flat. `Acme-Tenant-Id`, `Acme-Signature` — a vendor or product prefix makes accidental collision with a future standard or another vendor's field unlikely. `Tenant-Id` unscoped is a squat on a plausible future standard name. - **Token syntax.** ASCII letters, digits and hyphens; no spaces, no underscores if you can avoid them (underscores are legal in HTTP but some servers, notably default nginx, drop underscore-containing headers or require `underscores_in_headers on`, and CGI-style environments mangle `-` and `_` into each other). - **Case-insensitive.** `Acme-Tenant-Id` and `acme-tenant-id` are the same field. HTTP/2 and HTTP/3 require lowercase field names on the wire, so any code comparing names case-sensitively breaks when the protocol version changes. - **Do not re-prefix.** `X-` is out; so are private-use prefixes that imply a namespace HTTP does not have. ## Designing the value - **Define the syntax and write it down** — is it a single token, a comma-separated list, a quoted string? Multiple field lines with the same name are combined with commas by intermediaries, which silently breaks values that themselves contain commas unless you quote them. Structured Fields (RFC 8941) gives a rigorous, parseable value syntax and is the modern recommendation for new fields. - **ASCII only.** Non-ASCII bytes in field values are unsafe; encode (base64, percent-encoding) if you must carry them, and never assume UTF-8 survives. - **Keep it small.** Servers and proxies enforce per-field and total header size limits; oversized headers get rejected with 431 or a 400-class error from an intermediary you do not control. Headers are also repeated on every request — in HTTP/1.1 with no compression at all. - **No secrets, no PII.** Headers are logged by default almost everywhere, including by intermediaries you do not own. Credentials belong in `Authorization`, which tooling already treats as sensitive and which clients drop on cross-origin redirects. - **Check for an existing field first.** `Accept`, `Content-Type`, `If-Match`, `Retry-After`, `Forwarded`, `traceparent` — reinventing a standard field costs interoperability for nothing. - **Consider whether it belongs in a header at all.** Headers suit routing, tracing, conditional and content-negotiation metadata that an intermediary might act on. Business data usually belongs in the body or the URI, where it is versioned, validated and visible. ## Operational reality Custom fields are only useful if they survive the path. Load balancers, CDNs and service meshes may forward only known or allowlisted fields; some strip anything unrecognised. Browsers add another gate: a custom **request** header triggers a CORS preflight, and a custom **response** header is invisible to JavaScript unless named in `Access-Control-Expose-Headers`. Test the real path, not localhost. ## The honest exception `X-Request-Id`, `X-Forwarded-For` and friends are entrenched and understood by existing infrastructure. Continuing to emit them for compatibility is defensible; the RFC targets **new** names. What is not defensible is minting fresh `X-` fields in 2026 and calling it convention.

  • Does RFC 6648 mean existing X- headers should be renamed?
    No. It applies to new parameter names; renaming entrenched fields would break the infrastructure that already understands them. X-Forwarded-For and X-Request-Id keep working and keep being emitted for compatibility. The rule is simply not to mint new X- names, and to pick the permanent name up front instead.
  • What breaks if a custom header value contains a comma?
    Intermediaries may combine repeated field lines of the same name into one comma-separated value, and parsers commonly split on commas, so an unquoted comma inside your value becomes an accidental list separator. Quote the value or adopt RFC 8941 Structured Fields, which defines exactly how lists, items and parameters are encoded and parsed.

saying these in an interview costs you the question

  • Believing X- headers get special handling or a protected namespace from HTTP
  • Minting new X- names today because 'that is the convention for custom headers'
  • Comparing header names case-sensitively, which breaks under HTTP/2 and HTTP/3 lowercasing
  • Putting API keys, tokens or personal data in a custom header instead of Authorization
  • Assuming a custom header always reaches the application through CDNs, proxies and browsers without configuration

context