1. Library
  2. Computer Networks
  3. Http and the Web
  4. Headers

Updated 9 hours ago

Every HTTP request can carry extra information beyond what the standard headers provide. Custom headers are how applications talk to themselves—passing context, tracking requests, controlling features. They're the margin notes of HTTP.

What Custom Headers Actually Do

Standard headers handle universal concerns: caching, content types, authentication. But applications have their own concerns:

  • Which version of the API should handle this request?
  • How do I trace this request through fifty microservices?
  • Should this user see the new checkout flow?

Custom headers answer these questions:

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
X-API-Version: 2024-10-01
X-Feature-Flag: experimental-ui

These aren't part of any HTTP specification. They're agreements between your systems about what information to pass and how to interpret it.

The X- Prefix: A Brief History of Good Intentions

Custom headers traditionally start with "X-" to mark them as extensions:

X-Request-ID: 123
X-Powered-By: CustomServer/1.0

This convention came from email headers (RFC 822) and seemed sensible—clearly distinguish custom from standard.

Then reality happened. Some "experimental" headers became so widely used they were effectively standard. X-Forwarded-For is everywhere. But it still has the X- prefix, because removing it would break everything that depends on the old name.

RFC 6648 (2012) officially deprecated the X- prefix for new headers. The recommendation now: just use descriptive names.

Request-ID: 550e8400-e29b-41d4-a716-446655440000
API-Version: 2024-10-01

In practice, both conventions coexist. Legacy headers keep their X- prefix. New headers might or might not use it. The important thing is consistency within your own systems.

Request Tracing: The Killer Use Case

When a request fails at 3am in a system with fifty services, a request ID is the difference between finding the problem in minutes and searching blind through millions of log lines.

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Every service logs this ID with every action:

[550e8400...] API Gateway: Received request
[550e8400...] Auth Service: Token validated
[550e8400...] User Service: Fetching profile
[550e8400...] Database: Query timeout after 30s
[550e8400...] User Service: Error - upstream timeout
[550e8400...] API Gateway: Returning 500

Search for that ID and you see the entire story. The database timed out. The user service couldn't recover. The gateway returned an error. Without the ID, you'd be correlating timestamps and guessing.

The pattern is simple: generate an ID at the edge, pass it through every service, log it everywhere.

function requestIdMiddleware(request, response, next) {
    const requestId = request.headers['x-request-id'] || generateUUID();
    request.requestId = requestId;
    response.set('X-Request-ID', requestId);
    next();
}

API Versioning

APIs evolve. Old clients break. Custom headers offer one solution:

GET /api/users
API-Version: 2024-10-01

The same endpoint serves different responses based on the version header. Compare this to URL versioning:

GET /api/v2/users

Both work. Header versioning keeps URLs cleaner. URL versioning is more visible and cacheable. Choose based on your needs, but be consistent.

Feature Flags

Headers can control which features a request sees:

X-Feature-Flags: new-checkout,experimental-search

This enables:

  • A/B testing: Route different users to different experiences
  • Gradual rollouts: Enable features for 1% of traffic, then 10%, then everyone
  • Development: Test new features in production without exposing them

The server checks the flags and adjusts behavior accordingly.

Rate Limiting Information

APIs often tell clients how much capacity they have left:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1609459200

Clients can throttle themselves rather than hitting limits and getting errors. GitHub's API does this extensively—the headers tell you exactly how many requests you have left and when your quota resets.

Proxy Headers: The De Facto Standards

Some custom headers are so universal they're effectively standard.

X-Forwarded-For tracks client IPs through proxy chains:

X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178

Each proxy appends the IP it received from. The leftmost is the original client.

X-Forwarded-Proto records the original protocol:

X-Forwarded-Proto: https

Critical when load balancers terminate TLS and forward plain HTTP to backends. Without this header, the backend thinks every request is insecure.

X-Real-IP is nginx's simpler alternative—just the original client IP:

X-Real-IP: 203.0.113.195

Designing Good Custom Headers

Use clear, descriptive names:

// Clear
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
X-API-Version: 2024-10-01

// Cryptic
X-Req: 550e8400-e29b-41d4-a716-446655440000
X-V: 2024-10-01

One purpose per header:

// Good
X-Request-ID: 550e8400
X-User-ID: user-123

// Problematic
X-Context: requestId=550e8400,userId=user-123

Keep them small. Headers have practical limits (typically 8KB total). Move large data to the request body.

Handle missing headers gracefully:

const apiVersion = request.headers['x-api-version'] || '2024-10-01';

CORS and Custom Headers

Browsers restrict custom headers in cross-origin requests. For your custom headers to work:

The server must explicitly allow them in preflight responses:

Access-Control-Allow-Headers: X-Request-ID, X-API-Version

For clients to read custom response headers, expose them:

Access-Control-Expose-Headers: X-Request-ID, X-RateLimit-Remaining

Without these CORS headers, browsers silently block custom header access.

Security

Headers are visible. They're logged by proxies, cached by CDNs, stored in browser history. Never put secrets in headers:

// Dangerous
X-Admin-Token: secret-admin-token
X-Database-Connection: production-master

// Safe
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

Validate everything. Custom headers are user input:

const apiVersion = request.headers['x-api-version'];

if (!/^\d{4}-\d{2}-\d{2}$/.test(apiVersion)) {
    return response.status(400).json({ error: 'Invalid API version format' });
}

Implementation

Sending custom headers (client):

fetch('/api/data', {
    headers: {
        'X-Request-ID': crypto.randomUUID(),
        'X-API-Version': '2024-10-01'
    }
});

Reading custom headers (server):

app.get('/api/data', function(request, response) {
    const requestId = request.headers['x-request-id'];
    const apiVersion = request.headers['x-api-version'];
    
    response.set('X-Request-ID', requestId);
    response.json({ data: 'value' });
});

Key Takeaways

  • Custom headers carry application-specific metadata that standard headers don't address
  • The X- prefix is deprecated for new headers but ubiquitous in existing systems
  • Request IDs are essential for debugging distributed systems—generate at the edge, propagate everywhere
  • X-Forwarded-* headers are effectively standard for preserving client information through proxies
  • CORS requires explicit permission for custom headers in cross-origin requests
  • Never put sensitive data in headers—they're logged everywhere
  • Validate custom header values like any other user input

Frequently Asked Questions About Custom Headers

Was this page helpful?

😔
🤨
😃