What HTTP/2 leaks
When an HTTP/2 connection starts, the client sends a SETTINGS frame with up to six parameters. Each implementation has its own defaults:
- Chrome 148 sends a specific, documented combination —
HEADER_TABLE_SIZE: 65536, ordered set of settings. - Python httpx sends
HEADER_TABLE_SIZE: 4096, noMAX_HEADER_LIST_SIZE, different ordering. - curl default has yet another signature.
- Go net/http has its own set.
Beyond SETTINGS, the size of WINDOW_UPDATE frames, HPACK compression decisions, and the order of stream operations all create a secondary fingerprint that cannot be spoofed without rewriting the HTTP/2 client itself.
The SETTINGS frame in detail
The HTTP/2 SETTINGS frame is the first thing a client sends after the TLS handshake completes and the HTTP/2 preface (PRI * HTTP/2.0…SM) is exchanged. It declares the client's parameters as (id, value) pairs. Chrome 131 sends these six in this order:
| Setting | Chrome value | Common scraper mismatch |
|---|---|---|
HEADER_TABLE_SIZE | 65536 | Go: 4096 (default) |
ENABLE_PUSH | 0 | Go: 1, httpx: 1 |
MAX_CONCURRENT_STREAMS | 1000 | httpx: 100 |
INITIAL_WINDOW_SIZE | 6291456 | Go: 65535, httpx: 65535 |
MAX_FRAME_SIZE | 16384 | — usually correct |
MAX_HEADER_LIST_SIZE | 262144 | Not sent by Go default |
Immediately after SETTINGS, Chrome sends a WINDOW_UPDATE frame with a delta of 15663105 (15 MB minus the default 65535). Then it sends the request headers in HPACK-compressed form with the pseudo-header order :method, :authority, :scheme, :path — Firefox uses :method, :path, :authority, :scheme. Anti-bot vendors fingerprint all of: the SETTINGS values, their order, the WINDOW_UPDATE delta, and the pseudo-header order. curl_cffi bakes Chrome's values in; tls-client and noble-tls require manual configuration.
Why this matters more than scrapers realise
Akamai's EdgeWorker checks JA4 at the TLS layer. The next layer down checks HTTP/2 SETTINGS. If your JA4 says "Chrome 148" but your SETTINGS frame has HEADER_TABLE_SIZE: 4096 (Python httpx default), Akamai sees the contradiction and assigns maximum bot score before the HTML is served.
The mismatch is the signal. A clean JA4 with a Python HTTP/2 stack is worse than a clean JA4 with a Python TLS stack and HTTP/1.1 — the inconsistency itself is detectable.
What works
curl_cffi — wraps libcurl built against BoringSSL with Chrome's exact TLS and HTTP/2 parameters. impersonate="chrome131" handles both layers at once. This is the default first step for any Python scraper hitting a real anti-bot.
tls-client (Go, or Python wrapper) — same idea, built in Go, tends to be faster at 1k+ concurrent connections.
akamai-v3-sensor (Go) — for the hardest Akamai v3 targets that even curl_cffi cannot pass. Production scrapers run a small Go sidecar that uses a Chrome-compatible TLS + HTTP/2 stack at the C level, with Scrapy orchestrating the crawl.
Quick audit: hit tls.browserleaks.com/json from your scraper and from real Chrome. The response includes HTTP/2 SETTINGS — diff them. If they differ, your HTTP/2 layer is leaking.
