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. Think of it like recognising someone by their handwriting rather than their signature: when a client opens an HTTP/2 connection it sends a few low-level setup options, and every implementation fills them in slightly differently. Chrome 148 ships one combination, Python httpx another, Go net/http a third. Akamai and Cloudflare both fingerprint these. TLS is the encryption layer behind https; HTTP/2 sits just above it. So 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

The moment an HTTP/2 connection opens, the client sends a SETTINGS frame — a short message declaring up to six configuration values. Each implementation picks its own defaults, so the SETTINGS frame acts like a name tag:

  • 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.

The leak does not stop at SETTINGS. Other low-level details — the size of WINDOW_UPDATE frames (how much data the client says it is ready to receive), how HPACK header compression is applied, and the order in which streams are opened — form a secondary fingerprint. These are baked deep into the HTTP/2 client, so you cannot spoof them without rewriting the client itself.

The SETTINGS frame in detail

The SETTINGS frame is the first thing a client sends once the TLS handshake is done and the HTTP/2 preface (PRI * HTTP/2.0…SM, a fixed greeting that confirms both sides speak HTTP/2) has been exchanged. It lists the client's parameters as (id, value) pairs. Chrome 131 sends these six, in this exact 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

Right after SETTINGS, Chrome sends a WINDOW_UPDATE frame with a delta of 15663105 (15 MB minus the default 65535) — its way of saying it can buffer a lot of incoming data. Then it sends the request headers in HPACK-compressed form, with the pseudo-headers (the leading :-prefixed fields) in the order :method, :authority, :scheme, :path — Firefox uses :method, :path, :authority, :scheme. Anti-bot vendors fingerprint all of it: 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 (a fingerprint of the TLS handshake) at the TLS layer. The next layer down checks the HTTP/2 SETTINGS frame. If your JA4 says "Chrome 148" but your SETTINGS frame has HEADER_TABLE_SIZE: 4096 (the Python httpx default), Akamai sees the contradiction and assigns the maximum bot score before any 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 what gives you away.

What works

curl_cffi — wraps libcurl built against BoringSSL (Chrome's own TLS library) with Chrome's exact TLS and HTTP/2 parameters. One flag, 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 a Python wrapper) — same idea, built in Go, and 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: request tls.browserleaks.com/json from your scraper and from real Chrome. The response includes the 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 (the encrypted connection setup); HTTP/2 fingerprinting looks at the SETTINGS frame and stream-level behaviour one layer up. A scraper can match 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 stands out as an 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 does support HTTP/2, but it 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 a GET to 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 hash of your HTTP/2 behaviour). If the two 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 (the opening message of the encrypted handshake). 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, unmatched HTTP/2 implementation.

Last updated: 2026-05-31