JustBrowser
Tutorials14 min read

HTTP/2 Fingerprinting: How Akamai Spots Bots Before the Page Loads

JustBrowser Platform Team·
http2-fingerprintingakamai-bot-detectionnetwork-layer-detectionantidetect-browserbrowser-automationbuildinpublicsaasstudioaiworkforcebuildwithclaude

Last month I watched a scraping team burn through $1,200 in residential proxy credits debugging what they thought was an IP problem.

Their setup looked solid. JA4+ TLS fingerprints matched Chrome. User agent strings were current. Canvas and WebGL passed CreepJS. They'd followed our TLS fingerprinting guide and fixed those issues months ago.

But Akamai-protected sites kept blocking them. Not immediately — the TLS handshake completed, the connection established, then... challenge page. Every time.

I asked them to send me a packet capture. The problem was sitting in frame three.

Their Python scraper was using httpx with h2 as the HTTP/2 implementation. The SETTINGS frame it sent had HEADER_TABLE_SIZE: 4096, MAX_CONCURRENT_STREAMS: 100, INITIAL_WINDOW_SIZE: 65535. Those are h2's defaults.

Real Chrome sends HEADER_TABLE_SIZE: 65536, MAX_CONCURRENT_STREAMS: 1000, INITIAL_WINDOW_SIZE: 6291456. The mismatch was obvious. It took Akamai less than a second to notice.

That's HTTP/2 fingerprinting. The handshake your browser does after TLS but before loading any content — and most automation tools get it completely wrong.

(I'll be honest: when I first learned about this layer, I felt stupid for not checking it sooner. Years of blaming proxies.)

Check your own fingerprint first. We built a public checker at justbrowser.app/fingerprint-checker — 17 rows, client-side, no signup. It grades what it claims rather than guessing: WebRTC candidates are compared against your public IP, and the IP's location is graded on whether its UTC offset actually agrees with your browser's timezone. Worth running before you read the rest of this, so the signals below have your numbers attached to them.

What We're Building / Learning

By the end of this tutorial, you'll understand:

  1. How HTTP/2 fingerprinting works at the network layer
  2. Which specific frames and parameters expose automation
  3. Why HTTP libraries and stealth plugins can't fix this
  4. How to verify if your setup leaks through HTTP/2
  5. What native antidetect architecture does differently

This isn't a common detection vector in antidetect discussions. Most guides stop at TLS. But if you're hitting Akamai, Cloudflare, DataDome, or PerimeterX-protected targets and can't figure out why, HTTP/2 might be your leak. For teams also managing email deliverability alongside browser automation, JustEmails handles similar fingerprint consistency challenges at the SMTP layer.

Prerequisites

  • Basic understanding of HTTP/2 (you know what multiplexing means)
  • An antidetect browser or automation setup to test
  • Wireshark or Chrome DevTools Network tab with protocol column enabled
  • A target site using HTTP/2 (most major sites do)
  • 30-45 minutes

If you need background on TLS-layer fingerprinting, read the JA4+ guide first. For OS-level network fingerprinting (TCP/IP), we covered that in the p0f tutorial. This tutorial sits between TLS and the application layer.

Step 1: Understanding What HTTP/2 Fingerprinting Actually Measures

When your browser opens an HTTP/2 connection, the first thing it sends after the TLS handshake is a connection preface and a SETTINGS frame. This isn't optional. RFC 7540 requires it.

The SETTINGS frame contains parameters that tell the server how your client wants to handle the connection:

HEADER_TABLE_SIZE (0x1): Maximum size of the header compression table. Chrome uses 65536. Firefox uses 65535. Safari uses 4096.

ENABLE_PUSH (0x2): Whether server push is allowed. Most browsers send 0 (disabled) but the timing varies.

MAX_CONCURRENT_STREAMS (0x3): How many parallel streams you'll accept. Chrome sends 1000. Firefox sends 100. Node.js http2 defaults to unlimited.

INITIAL_WINDOW_SIZE (0x4): Flow control window for streams. Chrome uses 6291456 (6MB). Firefox uses 131072 (128KB). Massive difference.

MAX_FRAME_SIZE (0x5): Maximum frame payload. Usually 16384 across browsers.

MAX_HEADER_LIST_SIZE (0x6): Maximum header block size. Chrome sends 262144. Some browsers omit this entirely.

Detection systems don't just check individual values — they check combinations. Chrome's combination is different from Firefox's. Both different from libraries. That's the trap.

Here's what Chrome 125 sends (approximate — varies slightly by platform):

SETTINGS
  HEADER_TABLE_SIZE: 65536
  MAX_CONCURRENT_STREAMS: 1000
  INITIAL_WINDOW_SIZE: 6291456
  MAX_FRAME_SIZE: 16384
  MAX_HEADER_LIST_SIZE: 262144

Here's what Python's h2 library sends by default:

SETTINGS
  HEADER_TABLE_SIZE: 4096
  MAX_CONCURRENT_STREAMS: 100
  INITIAL_WINDOW_SIZE: 65535
  MAX_FRAME_SIZE: 16384

See the problem? Even if you've got perfect TLS fingerprints, perfect user agent, perfect everything else — your HTTP/2 SETTINGS frame screams "I'm a Python script."

Step 2: The Priority Tree Problem

HTTP/2 originally included PRIORITY frames that let browsers hint which resources matter most. Chrome used an elaborate priority tree — the main document at priority 256, CSS at 256, JavaScript at 220, images at 147 (roughly). The exact weights and dependencies created a fingerprint.

RFC 9218 deprecated this in favor of Extensible Priorities, but the transition is ongoing. Some browsers still send PRIORITY frames. Some use the newer Priority header. Detection systems check for both.

The issue for automation: HTTP libraries either skip priorities entirely or implement them incorrectly.

When you request a page with requests + h2:

# This opens a single stream with no priority information
async with httpx.AsyncClient(http2=True) as client:
    response = await client.get("https://target.com")

When Chrome requests the same page:

  1. Opens stream 1 (main document) at priority 256, depends on root
  2. Parses HTML, opens streams 3, 5, 7 for CSS at priority 256
  3. Opens streams 9, 11, 13 for JS at priority 220
  4. Images get lower priority, opened in document order

The dependency tree, stream IDs (always odd numbers for client-initiated), and opening order create a behavioral fingerprint that's distinct to each browser.

I spent a weekend trying to replicate Chrome's exact priority behavior in a Python script. Got about 80% there before giving up. Honestly, I'm not proud of those two days — my partner was not thrilled about me muttering "stream dependencies" during dinner. The edge cases are brutal — reprioritization on the fly, handling pushed streams, dealing with concurrent requests. Real browsers do all this automatically. Replicating it manually is a nightmare.

Step 3: WINDOW_UPDATE Timing

Flow control in HTTP/2 works through WINDOW_UPDATE frames. Your browser has a window of bytes it can receive before it must acknowledge them. Different browsers acknowledge at different thresholds with different timing.

Chrome tends to send WINDOW_UPDATE frames when the window is about 50% consumed. Firefox waits longer. Safari has its own pattern.

Detection systems that track timing characteristics notice these differences. If your SETTINGS frame claims you're Chrome but your WINDOW_UPDATE pattern matches Python's h2 library (which uses different heuristics), that's another signal.

This one is particularly hard to fake because it depends on your actual network conditions. The timing relationship between receiving data and sending WINDOW_UPDATE depends on your code's processing speed, not just configuration.

For teams running ad verification workflows, ClickzProtect factors HTTP/2 fingerprints into bot scoring — the same detection techniques work in reverse.

Step 4: How to Test Your Setup

First, verify what your current setup actually sends.

Using Wireshark:

  1. Start capture on your network interface
  2. Filter: http2
  3. Make a request to any HTTP/2 site (h2o.examp1e.net is a test server)
  4. Find the SETTINGS frame (usually third packet after TLS)
  5. Compare values to known browser signatures

Using Chrome DevTools:

  1. Open DevTools > Network
  2. Right-click column headers > Protocol
  3. Make requests and filter by h2
  4. You can't see raw SETTINGS here, but you can verify HTTP/2 is active

Using curl with verbose mode:

curl -v --http2 https://target.com 2>&1 | grep -A 20 "SETTINGS"

This shows curl's settings, which are different from any browser — useful for comparison.

Using h2spec for validation:

h2spec is a conformance testing tool. It won't tell you if you match a specific browser, but it will catch HTTP/2 implementation bugs that detection systems would flag.

If you're using JustBrowser, the SETTINGS frame matches real Chrome because it's actual Chrome — Chromium's nghttp2 stack, not a wrapper. Launch a profile, capture traffic, and compare. Should be identical to stock Chrome on the same platform.

Step 5: Why Stealth Plugins and Wrappers Can't Fix This

Here's where the architecture problem becomes clear.

Playwright, Puppeteer, and Selenium control a browser instance through CDP (Chrome DevTools Protocol). They can intercept network requests, modify headers, spoof JavaScript properties. But CDP doesn't expose HTTP/2 connection-level settings.

When you connect Playwright to Chrome, Chrome's built-in HTTP/2 stack handles the connection. That's fine — Chrome sends real Chrome settings. The problem comes when people use these tools with stealth plugins that try to claim a different browser identity.

Puppeteer-stealth can make navigator.userAgent say Firefox. It can spoof the User-Agent header. But the HTTP/2 SETTINGS frame still comes from Chrome's network stack, which runs below where puppeteer-stealth operates.

The mismatch: your headers say Firefox, your HTTP/2 says Chrome. Oops.

And look, I think the puppeteer-stealth maintainers know this. It's not their fault — CDP simply doesn't expose that layer. They're doing what they can with what Chrome gives them.

Extension-based antidetect browsers have the same problem. They wrap Chrome (or Chromium), injecting scripts to override fingerprint APIs. But they can't modify the nghttp2 implementation that handles HTTP/2. The connection-level fingerprint leaks their true identity.

This is why native architecture matters. JustBrowser is a C++ Chromium fork, and its network stack is stock Chromium — untouched. That is why the HTTP/2 SETTINGS frame matches real Chrome: it is real Chrome's implementation, not a library approximation. When a profile claims Chrome, every layer is Chrome: TLS handshake, HTTP/2 SETTINGS, priority behavior, WINDOW_UPDATE timing. And JustBrowser's Firefox profiles run real Firefox via Playwright injection, so they send Firefox's own HTTP/2 SETTINGS — the network layer is never faked at the wrapper level.

Step 6: Matching HTTP/2 to Your Target

Different detection vendors weight HTTP/2 fingerprinting differently. This helps you understand why a legitimate automation or testing setup gets challenged, and it works best alongside the target site's own terms and API or access options. If a site forbids automated access, use its official route rather than a workaround.

Akamai Bot Manager: Heavy reliance on HTTP/2 fingerprinting. They correlate SETTINGS values, stream ordering, and priority behavior with TLS fingerprints and JavaScript fingerprints. Akamai publishes research on this — they're not subtle about it. If you're hitting Akamai-protected sites, HTTP/2 matters a lot.

Cloudflare Bot Management: Uses HTTP/2 fingerprinting as one signal among many. They're more focused on behavioral signals and TLS, but HTTP/2 mismatches contribute to risk scoring. Check out our guide on browser fingerprint testing for related detection vectors.

DataDome: Strong HTTP/2 analysis. They specifically look for library fingerprints (h2, httpcore, aiohttp patterns). DataDome's published research mentions connection-level fingerprinting explicitly.

PerimeterX (now HUMAN): Historically more JavaScript-focused, but their newer detection includes network-layer signals. HTTP/2 is part of their stack.

(Hot take: Akamai leans on network-layer signals the most right now. Cloudflare weights behavioral scoring more heavily. DataDome sits in the middle. Your mileage may vary — I've seen teams who disagree completely.)

The practical implication: if you're scraping Akamai-protected targets with raw HTTP clients, you're fighting uphill. Even with perfect TLS fingerprints (and most libraries don't have those either), the HTTP/2 layer gives you away. For payment processing workflows where trust signals matter equally, VeloCards handles the transaction-side fingerprinting considerations.

Common Errors and How to Fix Them

Error: Blocked immediately after connection on Akamai sites

You're probably using an HTTP library with mismatched HTTP/2 settings. The fix isn't "better proxies" — it's using a real browser. Akamai is comparing your SETTINGS frame to your claimed identity and finding a contradiction.

Error: Challenge pages despite passing TLS fingerprint checks

If you've fixed JA4+ and still get challenges, HTTP/2 is the likely culprit. Capture your traffic and compare SETTINGS against browser baselines. For targets with stringent bot detection, consider pairing your scraping with JustAnalytics for conversion tracking that doesn't introduce additional fingerprint surface.

Error: Inconsistent blocking (works sometimes, fails other times)

Some detection systems sample requests rather than checking every one. You might pass 3 requests and fail the 4th. This isn't randomness — it's either IP reputation shifts or A/B testing of detection rules. The HTTP/2 fingerprint is still wrong; you're just getting lucky some of the time.

Error: "Bot detected" JSON response instead of HTML

Several CDNs return a JSON error for programmatic clients. This often means the HTTP/2 fingerprint identified you as non-browser before the request even completed. Real browsers don't get JSON error responses — they get challenge pages or CAPTCHAs.

Error: Playwright/Puppeteer works locally but fails in production

Local testing often hits the same site repeatedly from the same IP without rate limits. Production scraping triggers more aggressive detection. But also: local Chrome might be headed mode while production is headless. Headless Chrome has slightly different HTTP/2 behavior in some configurations. If you run headed in production, do it on the Windows or Mac machine that hosts JustBrowser (the API's {"headless": true} option exists if you need it, but headed is safer for hard targets).

Next Steps

HTTP/2 fingerprinting is one layer in the stack. If you're building serious scraping or multi-account infrastructure, you need consistency across all layers:

For teams managing multiple platforms, VeloCalls handles phone-based verification alongside browser fingerprinting — different signal, same trust problem.

The JustBrowser trial runs 7 days with native HTTP/2 handling and nothing held back — unlimited profiles, API access included. That's enough to verify whether a native approach fixes your detection issues before you're charged. It's $9.99/mo after that, or $99.99/year.

The honest take: if your targets don't use Akamai-level bot detection, you might not need to worry about HTTP/2 fingerprinting. Plenty of sites only check JavaScript fingerprints. But if you're hitting enterprise targets with serious security budgets, every layer matters. HTTP/2 is one of the harder ones to get right — and one of the harder ones for detection to explain in their block pages. You'll just see "access denied" without knowing why.

My frustration with this layer? There's no feedback loop. TLS fingerprint checkers exist. Canvas test pages exist. Nobody built a public HTTP/2 fingerprint checker. You're flying blind unless you packet-capture everything.

That's the nature of network-layer detection. It's invisible until you start packet-capturing. And by then, you've already been flagged.

Frequently Asked Questions

What is HTTP/2 fingerprinting and how does it detect bots?

HTTP/2 fingerprinting analyzes the SETTINGS frame your browser sends immediately after the TLS handshake. This frame contains values like HEADER_TABLE_SIZE, MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, and MAX_HEADER_LIST_SIZE. Each browser sets these values differently — Chrome uses 65536 for HEADER_TABLE_SIZE while Firefox uses 65535. Combined with WINDOW_UPDATE behavior and priority pseudo-headers, detection systems build a signature that identifies your real browser before any page content loads.

Why can't HTTP libraries like requests or urllib match real browser HTTP/2 fingerprints?

Raw HTTP libraries use their own HTTP/2 implementations (h2 in Python, net/http in Go) with default settings that don't match any real browser. Chrome sends specific SETTINGS values, uses a particular priority tree structure, and emits frames in a specific order. Libraries skip browser-specific behaviors entirely. Detection systems see the mismatch — your TLS says Chrome but your HTTP/2 settings say python-h2 — and flag you immediately.

How does Akamai's bot detection use HTTP/2 fingerprinting?

Akamai correlates multiple signals: the SETTINGS frame parameters, the WINDOW_UPDATE timing, the PRIORITY or PRIORITY_UPDATE frames, and how streams are opened relative to each other. They build a profile of expected behavior per browser. When your HTTP/2 signature contradicts your claimed user agent or TLS fingerprint, Akamai flags the mismatch. This happens at the connection level before any JavaScript executes — stealth plugins can't patch what they can't reach.

Does JustBrowser handle HTTP/2 fingerprinting?

JustBrowser does not modify the HTTP/2 layer at all — it ships Chromium's own network stack unchanged, so SETTINGS frames, WINDOW_UPDATE behavior and priority handling are exactly what real Chrome sends. Nothing to spoof, nothing to leak. When you launch a profile claiming Chrome, the HTTP/2 fingerprint is genuine Chrome — not a library approximation. That's architecturally out of reach for HTTP clients pretending to be browsers.


Try JustBrowser

Native Chromium antidetect browser — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. REST API for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.

Get started → · How it differs from Multilogin / GoLogin / AdsPower

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy