Why JWT Size Matters
A JWT is transmitted on every single HTTP request your authenticated client makes. A 500-byte token in a cookie means 500 bytes of outbound bandwidth per request, consumed by both the client (upload bandwidth, mobile battery) and the server (parsing time, header buffer memory). At scale, token size becomes an infrastructure cost line: a 2 KB JWT on a 1 M requests-per-day API eats 2 GB of header-only bandwidth per day — and that is the smaller of the problems. Once a token exceeds infrastructure limits, requests fail outright with cryptic errors.
Size Breakdown by Segment
| Segment | Typical length (chars = bytes) | Notes |
|---|---|---|
| Header | 25–40 | Depends on typ and kid presence |
| Payload (minimal) | 60–120 | Just sub, iat, exp |
| Payload (typical) | 200–500 | +iss, aud, jti, roles |
| Payload (large) | 500–1500 | Full user profile, scopes, custom claims |
| HS256 signature | 43 | Fixed — 32 bytes Base64URL |
| HS384 signature | 64 | Fixed — 48 bytes Base64URL |
| HS512 signature | 86 | Fixed — 64 bytes Base64URL |
| ES256 signature | 86 | ECDSA P-256 — compact asymmetric |
| RS256 signature | 342 | RSA-2048 — bloats the token |
| RS512 signature | 342 | Same key size, same output |
| Dots (separators) | 2 | Fixed |
Infrastructure Size Limits
| Location | Default limit | Error on exceed |
|---|---|---|
| HTTP cookie (RFC 6265) | 4,096 bytes | Silent cookie truncation / drop |
| Nginx header line | 8 KB (large_client_header_buffers) | 400 Bad Request |
| Apache header line | 8,190 bytes (LimitRequestFieldSize) | 400 Bad Request |
| IIS header | 16 KB | 400 Bad Request |
| Node.js http.server | 80 KB total (--max-http-header-size) | HTTP 431 |
| Cloudflare request headers | 16 KB total | 400 / 494 |
| AWS ALB | 24 KB per header, 60 KB total | 400 |
| Browser URL (query string) | ~2,000–8,000 chars | Truncation / 414 |
Diagnosing a 431 Error
- Reproduce with
curl -v YOUR_URL— the response headers show which server rejected it. - Count header bytes:
curl -s -o /dev/null -D - YOUR_URL -H "Authorization: Bearer $JWT" -H "Cookie: $COOKIE"→ sum theRequestsection bytes. - Isolate — remove the JWT. If 431 goes away, the token is the culprit. If not, other headers (like oversized Referer, custom tracing headers) are at fault.
- Check for duplication — browsers often send the JWT as both
CookieandAuthorization. Pick one. - Decode the token (use the decoder above) and look at the payload size. If over 500 bytes, see "Trimming Techniques" below.
Trimming Techniques (Ranked by Impact)
- Switch RS256 → ES256 or HS256. Saves ~250 bytes immediately. Only applies if you control the issuer. ES256 keeps asymmetric keys (needed for third-party verification).
- Move user profile out of the payload. Store
name,email,avatar_url,preferencesin your database, keyed bysub. The JWT only needs to identify the user — the application looks up the rest. - Compress role / scope lists.
"scope": "read write admin"(23 bytes) beats"scopes": ["read", "write", "admin"](34 bytes). RFC 6749 §3.3 specifies space-delimited. - Shorten custom claim names.
"t": "org_1"instead of"tenant_id": "org_1". Trade readability for bandwidth; worth it on high-RPS APIs. - Drop URI-namespaced claims for internal services.
"https://myapp.com/role": "admin"wastes 24 bytes vs"role": "admin". Namespacing only matters when tokens cross organisational boundaries. - Reference-token pattern. If you still need to carry 20+ fields of per-user data, put a session ID in the JWT (
"sid": "sess_a8b3c4d5") and store the heavy payload in Redis. The JWT remains compact; the app looks up the session on request. - Drop
jtiif you do not use revocation. UUIDs are 36 bytes; GUIDs 32. If you cannot revoke tokens anyway, you are paying for a feature you do not use.
Measuring Each Segment
// JavaScript — breakdown by segment
const jwt = 'eyJhbGciOiJIUzI1NiI...';
const [header, payload, signature] = jwt.split('.');
console.log({
total: jwt.length,
header: header.length,
payload: payload.length,
signature: signature.length,
overhead: 2, // the two dots
decoded_header: atob(header).length, // what the actual JSON looks like
decoded_payload: atob(payload).length,
});
# Python — same breakdown
import base64
header, payload, sig = jwt.split('.')
print({
'total': len(jwt),
'header': len(header),
'payload': len(payload),
'signature': len(sig),
'decoded_header_json': len(base64.urlsafe_b64decode(header + '==')),
'decoded_payload_json': len(base64.urlsafe_b64decode(payload + '==')),
})Base64URL Encoding Math
Base64URL encoding adds roughly 33% to raw byte size: N bytes of JSON becomeceil(N/3) × 4 Base64URL characters, minus any = padding (removed in JWT per RFC 7515). Concrete examples: 32 bytes → 43 chars, 48 bytes → 64 chars, 100 bytes → 134 chars. Rule of thumb: multiply the decoded JSON size by 1.33 to estimate the encoded JWT size.
Related JWT Tools
- JWT Decoder Online — decode and measure a token
- JWT Payload Example — understand what inflates payload size
- JWT HS256 Decoder — the smallest-signature algorithm
- JWT ES256 Decoder — compact asymmetric alternative to RS256
- JWT RS256 Decoder — the token-bloating but widely-used algorithm
- JWT Debugger — full decode and verify workflow