What Is HTTP/2 Fingerprinting Explained
HTTP/2 fingerprinting is the practice of identifying clients by how they configure the HTTP/2 protocol at the binary framing layer — before any application data is exchanged. While TLS fingerprinting (JA3/JA4) captures the ClientHello packet, HTTP/2 fingerprinting inspects the SETTINGS frame, WINDOW_UPDATE frame, stream priority tree, and pseudo-header order. The result is a compact string — often called the Akamai HTTP/2 fingerprint — that distinguishes a real Chrome browser from a Python httpx client with near-certainty, before a single HTML byte is returned.
If your scraping stack sends a Chrome-shaped TLS ClientHello but an httpx-shaped h2 SETTINGS frame, anti-bot systems don't need to evaluate your JavaScript or check your CAPTCHA token. The protocol mismatch alone is enough to assign a maximum bot score before any HTML loads.
The H2 Settings Frame: What Gets Fingerprinted
HTTP/2, defined in RFC 9113, requires every connection to begin with a SETTINGS frame carrying key-value pairs that configure the connection. Real browsers send a specific, consistent set of these parameters — and most HTTP client libraries send a different, much smaller set.
The parameters that matter for fingerprinting:
- HEADER_TABLE_SIZE (ID 1): The HPACK dynamic header table size. Chrome sends
65536bytes; the Python h2 library (used by httpx) defaults to4096bytes. - ENABLE_PUSH (ID 2): Whether server push is enabled. Chrome sends
0(disabled since Chrome 76); many libraries leave it at the spec default of1or omit it. - INITIAL_WINDOW_SIZE (ID 4): The flow-control window for streams. Chrome sends
6291456bytes (6 MB); httpx defaults to65535bytes (64 KB). - MAX_HEADER_LIST_SIZE (ID 6): Maximum acceptable header list size. Chrome sends
262144bytes (256 KB); most libraries don't send this parameter at all.
Beyond SETTINGS, two additional signals are captured:
- WINDOW_UPDATE: After the SETTINGS frame, the client sends a connection-level WINDOW_UPDATE. Chrome sends
15663105— a value so specific that its absence is itself a signal. - Pseudo-header order: HTTP/2 uses pseudo-headers (
:method,:authority,:scheme,:path). Chrome sends them asm,a,s,p. Some clients reorder these, creating an immediate mismatch.
The Akamai h2 fingerprint concatenates all of this into a compact string:
# Chrome 148 fingerprint
1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p
# httpx (Python h2 library) fingerprint
1:4096|65535|0|m,a,s,p
The difference is stark. The httpx fingerprint is missing four of Chrome's six settings, has a 16× smaller header table, and a 96× smaller initial window. No heuristic analysis needed — a simple string comparison flags this as non-browser.
JA4H: The Application-Layer Companion
While the Akamai h2 fingerprint captures the protocol layer, JA4H — part of the JA4 suite by FoxIO — captures the HTTP application layer. JA4H encodes the HTTP method, version, cookie presence, header count, and sorted header names into a fingerprint string like ge20cn05acijefhmsu.
JA4H and the h2 fingerprint are complementary. A sophisticated anti-bot system checks both: the h2 fingerprint identifies the protocol implementation, while JA4H identifies the HTTP header profile. If either doesn't match the claimed browser, the request is flagged.
Why Protocol Mismatches Trigger Max Bot Scores
Here's the scenario that gets you blocked before your request reaches the application layer:
- Your TLS ClientHello matches Chrome 148 (JA4 =
t13d1516h2_...), because you configured your TLS library to impersonate Chrome's cipher order and extensions. - Your HTTP/2 SETTINGS frame still carries h2 library defaults:
HEADER_TABLE_SIZE=4096,INITIAL_WINDOW_SIZE=65535, noMAX_HEADER_LIST_SIZE. - The anti-bot system compares your JA4 (says "Chrome 148") against your h2 fingerprint (says "Python h2 library"). They don't agree.
- You're assigned a maximum bot score — often 90+ out of 100. No JavaScript challenge is issued. No CAPTCHA is shown. The connection is rejected or returned a 403.
This is why simply patching your TLS fingerprint (using a library like tls-client or configuring BoringSSL cipher order) is insufficient. The anti-bot vendor doesn't need JavaScript execution to catch you — the protocol layer already told them you're lying about your identity.
TLS and HTTP/2 Coherence: The Full Stack Problem
Anti-bot detection is a coherence check, not a single-signal test. The system asks: "Do all signals agree?" If your JA3/JA4 says Chrome, your h2 fingerprint says Chrome, your JA4H says Chrome, and your User-Agent says Chrome — you pass. If any one signal disagrees, you fail.
| Layer | Fingerprint | What It Captures | Example (Chrome 148) |
|---|---|---|---|
| TLS | JA3 / JA4 | ClientHello cipher order, extensions, curves | t13d1516h2_... |
| HTTP/2 | Akamai h2 fingerprint | SETTINGS, WINDOW_UPDATE, priority, pseudo-header order | 1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p |
| HTTP headers | JA4H | Method, version, header count, header names | ge20cn05acijefhmsu |
| JavaScript | Canvas / WebGL / navigator | Browser API fingerprints, feature detection | Chrome-specific APIs present |
Most Python and Node.js HTTP clients leak mismatched signals at the h2 layer:
- httpx (Python): Uses the h2 library, which sends
HEADER_TABLE_SIZE=4096and omits most Chrome settings. The TLS layer won't match the h2 layer. - requests (Python): Doesn't support HTTP/2 at all. If your TLS ClientHello advertises h2 via ALPN but requests falls back to HTTP/1.1, that creates a TLS/h2 mismatch.
- Node.js http2 module: Sends its own default SETTINGS (
HEADER_TABLE_SIZE=4096,ENABLE_PUSH=1), which don't match any real browser. - undici (Node.js): Uses Node's built-in h2 implementation with similar non-browser defaults.
- Go net/http2: Sends
HEADER_TABLE_SIZE=4096andINITIAL_WINDOW_SIZE=65535— the same giveaway as httpx.
Why Residential Proxies Are Still Required
Even with a perfectly coherent fingerprint — matching TLS, h2, and HTTP headers — you still need the right IP. Protocol spoofing alone fails because anti-bot systems apply IP-reputation scoring independently of protocol analysis.
Here's how it works in practice:
- Your h2 fingerprint matches Chrome perfectly. Your JA4 matches Chrome. Your headers are indistinguishable from a real browser.
- Your request comes from a datacenter IP range belonging to AWS, DigitalOcean, or a known VPS provider.
- The anti-bot system's IP-reputation database flags the IP as datacenter. Datacenter IPs carry a high baseline risk score — often 70–90 out of 100 before any other signal is evaluated.
- Your coherent fingerprint brings the score down, but not enough. A datacenter IP with a perfect browser fingerprint is still more suspicious than a residential IP with a slightly imperfect one.
Residential proxies solve this by routing your traffic through real ISP-assigned IP addresses. The IP-reputation check returns a low risk score (0–20), and your coherent fingerprint does the rest. This is why combining protocol-level spoofing with residential proxy locations is the standard approach for serious scraping operations. For more on this, see our web scraping use case guide.
Mobile proxies offer an even lower baseline risk score, as carrier IPs are treated as highly trusted by most reputation databases. However, residential proxies provide a better balance of cost, availability, and geographic coverage for most use cases.
Practical Implementation: Coherent Fingerprints with ProxyHat
There are two legitimate approaches to presenting a coherent h2 fingerprint: using a browser-impersonation library that reproduces Chrome's exact h2 settings, or using a real browser routed through a proxy. Full configuration details are available in the ProxyHat documentation.
Approach 1: curl_cffi with ProxyHat Residential Proxies
The curl_cffi library wraps libcurl-impersonate, which patches curl's TLS and HTTP/2 implementation to match real browsers byte-for-byte. When you specify impersonate="chrome", it sends Chrome's exact TLS ClientHello and Chrome's exact h2 SETTINGS frame — including the correct WINDOW_UPDATE and pseudo-header order.
from curl_cffi import requests
# ProxyHat residential proxy — US exit
proxies = {
"http": "http://user-country-US:YOUR_PASSWORD@gate.proxyhat.com:8080",
"https": "http://user-country-US:YOUR_PASSWORD@gate.proxyhat.com:8080"
}
# Impersonate Chrome 148 — TLS + h2 + headers all coherent
response = requests.get(
"https://httpbin.org/headers",
impersonate="chrome",
proxies=proxies,
timeout=30
)
print(f"Status: {response.status_code}")
print(response.text)
For sticky sessions (useful for multi-page flows that need the same IP):
from curl_cffi import requests
proxies = {
"http": "http://user-session-checkout01-country-US:YOUR_PASSWORD@gate.proxyhat.com:8080",
"https": "http://user-session-checkout01-country-US:YOUR_PASSWORD@gate.proxyhat.com:8080"
}
response = requests.get(
"https://example.com/login",
impersonate="chrome",
proxies=proxies
)
Approach 2: Real Browser via Playwright + ProxyHat
For targets with advanced JavaScript challenges (Cloudflare Turnstile, Akamai Bot Manager, DataDome), a real browser is the most reliable option. Playwright launches an actual Chromium instance, which naturally sends the correct h2 SETTINGS frame.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={
"server": "http://gate.proxyhat.com:8080",
"username": "user-country-US",
"password": "YOUR_PASSWORD"
}
)
page = browser.new_page()
page.goto("https://example.com")
content = page.content()
print(content[:500])
browser.close()
The trade-off: a real browser is 5–10× slower than curl_cffi and uses significantly more memory. For high-volume SERP tracking or price monitoring, curl_cffi is the better choice. For targets that require JavaScript rendering, use a browser.
SOCKS5 Alternative
If you need SOCKS5 (e.g., for certain tunneling setups), ProxyHat supports SOCKS5 on port 1080:
from curl_cffi import requests
proxies = {
"http": "socks5://user-country-US:YOUR_PASSWORD@gate.proxyhat.com:1080",
"https": "socks5://user-country-US:YOUR_PASSWORD@gate.proxyhat.com:1080"
}
response = requests.get(
"https://httpbin.org/ip",
impersonate="chrome",
proxies=proxies
)
print(response.json())
Common Mistakes and Edge Cases
Mistake 1: Patching TLS but Not h2
The most common mistake is using a TLS-impersonation library without also fixing the h2 layer. Your JA4 says Chrome, but your h2 SETTINGS say Python. The mismatch is immediately detectable by any system that checks both layers — and most modern anti-bot systems do.
Mistake 2: Using HTTP/1.1 with an h2-Advertised ALPN
If your TLS ClientHello advertises h2 in the ALPN extension but your client only speaks HTTP/1.1, the server may reject the connection or flag it as anomalous. Ensure your client actually implements HTTP/2 when you advertise it via ALPN.
Mistake 3: Inconsistent Pseudo-Header Order
Some custom h2 implementations send pseudo-headers in the order m,s,a,p or m,p,a,s instead of Chrome's m,a,s,p. This alone can trigger detection on strict anti-bot systems that include pseudo-header order in their fingerprint string.
Mistake 4: Ignoring WINDOW_UPDATE
The connection-level WINDOW_UPDATE value is part of the Akamai fingerprint. If your client sends a different value than the browser you're impersonating, the fingerprint won't match. Chrome's 15663105 is distinctive; sending 65535 instead is a dead giveaway.
Mistake 5: Using Datacenter Proxies with a Perfect Fingerprint
A perfect browser fingerprint from a datacenter IP is more suspicious than an imperfect fingerprint from a residential IP. Always pair protocol-level spoofing with residential proxy infrastructure to pass IP-reputation scoring.
Appropriate Use and Legal Considerations
HTTP/2 fingerprinting research and browser-impersonation techniques have legitimate applications:
- Authorized security testing: Penetration testers evaluating anti-bot protections for clients who own the target infrastructure.
- Competitive price monitoring: Collecting publicly available pricing data from e-commerce sites, in compliance with their terms of service.
- SERP tracking: Monitoring search engine rankings for SEO purposes — a well-established, legitimate use case.
- Academic research: Studying anti-bot systems, web measurement, and protocol analysis.
However, these techniques must not be used for fraud, credential stuffing, circumventing access controls, or violating the Computer Fraud and Abuse Act (CFAA) in the US or the General Data Protection Regulation (GDPR) in the EU. Always review the target's Terms of Service and robots.txt. If you're unsure whether your use case is legal, consult with a qualified attorney. ProxyHat's infrastructure is designed for legitimate automation, research, and monitoring — we do not support or condone use of our proxies for fraudulent activity or violations of applicable law.
Key Takeaways
- HTTP/2 fingerprinting captures the SETTINGS frame, WINDOW_UPDATE, stream priority, and pseudo-header order — all sent before any HTML loads.
- The Akamai h2 fingerprint is a compact string like
1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,pthat uniquely identifies browser implementations.- JA4H complements the h2 fingerprint by capturing the HTTP header profile. Both must agree with your TLS fingerprint (JA3/JA4).
- Most Python and Node.js clients (httpx, Node http2, Go net/http2) leak non-browser h2 settings frames with
HEADER_TABLE_SIZE=4096andINITIAL_WINDOW_SIZE=65535.- Protocol spoofing alone is insufficient — residential proxies are required to pass IP-reputation scoring.
- Use curl_cffi with
impersonate="chrome"for a coherent TLS+h2 fingerprint, routed through ProxyHat residential exits atgate.proxyhat.com:8080.- For JavaScript-heavy targets, use a real browser (Playwright) through ProxyHat — it naturally sends the correct h2 SETTINGS.






