Anti-Bot

What Is HTTP/2 Fingerprinting?

What Is HTTP/2 Fingerprinting? — conceptual illustration
On this page

HTTP/2 fingerprinting identifies an HTTP client from its SETTINGS frame and frame-level behaviour, independent of the TLS layer. Every HTTP/2 implementation sends slightly different defaults — Chrome 148 ships one combination, Python httpx another, Go net/http a third. Akamai and Cloudflare both fingerprint these. If your HTTP/2 layer does not match the browser you claim to be at the TLS layer, you flag at the network layer before the page even loads.

Quick facts

What it fingerprintsSETTINGS frame parameters and their order
Six parametersHEADER_TABLE_SIZE, MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, MAX_FRAME_SIZE, MAX_HEADER_LIST_SIZE, ENABLE_PUSH
Also fingerprintedHPACK header compression, WINDOW_UPDATE sizes, frame order
Vendors using itAkamai, Cloudflare (both at edge alongside JA4)
Bypassed bycurl_cffi, tls-client — they ship Chrome's exact HTTP/2 stack

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, no MAX_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:

SettingChrome valueCommon scraper mismatch
HEADER_TABLE_SIZE65536Go: 4096 (default)
ENABLE_PUSH0Go: 1, httpx: 1
MAX_CONCURRENT_STREAMS1000httpx: 100
INITIAL_WINDOW_SIZE6291456Go: 65535, httpx: 65535
MAX_FRAME_SIZE16384— usually correct
MAX_HEADER_LIST_SIZE262144Not 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.

Code example

python
# Audit your HTTP/2 fingerprint against real Chrome
from curl_cffi import requests

resp = requests.get(
    "https://tls.browserleaks.com/json",
    impersonate="chrome131",
)
fp = resp.json()
print("JA4:        ", fp["ja4"])
print("HTTP/2:     ", fp["akamai"])          # Akamai hash of HTTP/2 settings
print("User-Agent: ", fp["user_agent"])

# Compare these values against what real Chrome reports on the same page.
# If JA4 matches but HTTP/2 does not, fix your HTTP/2 stack first.

Related terms

Concept map

How HTTP/2 Fingerprinting connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Is HTTP/2 fingerprinting separate from JA4?

Yes — they are independent layers. JA4 fingerprints the TLS handshake; HTTP/2 fingerprinting fingerprints the SETTINGS frame and stream-level behaviour. A scraper can pass JA4 perfectly and still flag at the HTTP/2 layer. Most production scraping libraries fix both.

Can I just disable HTTP/2 to avoid this?

Disabling HTTP/2 forces HTTP/1.1, which is itself unusual in 2026. Most modern browsers prefer HTTP/2 or HTTP/3, so a client that refuses HTTP/2 is a noticeable anomaly. Fixing the SETTINGS frame is the right answer, not avoiding the protocol.

Does requests support HTTP/2?

No, the standard Python requests library is HTTP/1.1 only. httpx supports HTTP/2 but ships its own SETTINGS defaults that do not match Chrome. Use curl_cffi or tls-client if you need a Chrome-identical HTTP/2 stack.

How do I check my HTTP/2 fingerprint?

The simplest test is GET https://tls.browserleaks.com/json from both your scraper and real Chrome on the same machine. Compare the akamai field in the JSON response (Akamai's HTTP/2 hash). If they differ, your HTTP/2 layer is leaking.

Is the HTTP/2 fingerprint really separate from the TLS fingerprint?

Yes. JA4 captures the TLS Client Hello. JA4H captures the HTTP/2 client preface, the SETTINGS frame, and the pseudo-header order. A request can match Chrome on JA4 but fail JA4H — common with libraries that wrap a Chrome-impersonating TLS stack around their own HTTP/2 implementation.

Last updated: 2026-05-27