HTTP/2 Fingerprinting: What Your Connection Reveals Before the Page Loads

12 min read

Most fingerprinting discussions stop at the TLS handshake and JavaScript APIs. There is a layer in between that is easy to overlook: the HTTP/2 connection setup your client performs right after TLS and before any content loads.

Take a Python client using httpx with h2 as the HTTP/2 implementation. The SETTINGS frame it sends has HEADER_TABLE_SIZE: 4096, MAX_CONCURRENT_STREAMS: 100, INITIAL_WINDOW_SIZE: 65535. Those are h2's defaults.

Chrome sends HEADER_TABLE_SIZE: 65536 and INITIAL_WINDOW_SIZE: 6291456, and leaves MAX_CONCURRENT_STREAMS out entirely. A server that compares the two can tell, within the first few frames, that the client is not a mainstream browser, whatever its user agent string says.

That's HTTP/2 fingerprinting. It is useful to understand if you run a site and want to know what bot-management products see, if you test your own pages with automation, or if you simply want to know what your connection reveals about the software behind it.

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 identify a client
  3. Why raw HTTP libraries look different from browsers, and why page-level scripts can't change it
  4. How to inspect what your own setup sends over HTTP/2
  5. What a native browser architecture means for this layer

Most guides stop at TLS. If you operate a site behind Akamai, Cloudflare, DataDome, or PerimeterX, or you test your own pages with automation and wonder why some clients get challenged, HTTP/2 is part of the picture.

Prerequisites

  • Basic understanding of HTTP/2 (you know what multiplexing means)
  • A browser or automation setup you want to inspect
  • 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. 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 and Firefox use 65536. Python's h2 library defaults to 4096.

ENABLE_PUSH (0x2): Whether server push is allowed. Chrome sends 0 (disabled); not every client sends it at all.

MAX_CONCURRENT_STREAMS (0x3): How many parallel streams you'll accept. Current Chrome leaves it out. Python's h2 sends 100. Node.js http2 defaults to effectively 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. 16384, the protocol default, wherever it's sent.

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 differ from libraries. That's what makes the frame useful to a server.

Here's what recent Chrome sends (it can change between releases):

SETTINGS
  HEADER_TABLE_SIZE: 65536
  ENABLE_PUSH: 0
  INITIAL_WINDOW_SIZE: 6291456
  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

Even with a Chrome-like TLS handshake and a Chrome user agent, the HTTP/2 SETTINGS frame says "this is 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 practical difference: HTTP libraries either skip priorities entirely or implement them differently from browsers.

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.

The edge cases are brutal — reprioritization on the fly, handling pushed streams, dealing with concurrent requests. Real browsers do all this automatically, which is why a library's connection looks so different from a browser's.

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, for example, sends a large connection-level WINDOW_UPDATE straight after its SETTINGS frame, and other clients size and time theirs differently.

Systems that track timing characteristics notice these differences. If a SETTINGS frame looks like Chrome but the WINDOW_UPDATE pattern matches Python's h2 library (which uses different heuristics), that is another signal.

This one depends on actual network conditions. The timing relationship between receiving data and sending WINDOW_UPDATE depends on the client's processing speed, not just its configuration.

For site owners, the same signals are what bot scoring is built on. ClickzProtect, our passive bot detection platform, is one product in that space.

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 a server could notice.

If you're using JustBrowser, the SETTINGS frame matches Chrome because it's Chromium's own HTTP/2 implementation, not a wrapper. Launch a profile, capture traffic, and compare. Should be identical to stock Chrome on the same platform, because the network stack is unchanged.

Step 5: Why Page-Level Scripts and Wrappers Don't Touch This Layer

Here's where the architecture matters.

Playwright, Puppeteer, and Selenium control a browser instance through CDP (Chrome DevTools Protocol). They can intercept network requests, modify headers, and evaluate JavaScript. 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, and it sends Chrome's own settings. That is the expected behavior.

The layers only disagree when something changes the page-visible identity without changing the network stack. A script can make navigator.userAgent say Firefox and rewrite the User-Agent header, but the HTTP/2 SETTINGS frame still comes from Chrome's network stack, which runs below where the script operates. Headers say Firefox, HTTP/2 says Chrome: an inconsistency a server can see.

Extension-based browsers have the same limit. They wrap Chrome or Chromium and inject scripts, but they can't modify the HTTP/2 implementation underneath.

This is why native architecture matters. JustBrowser is a custom Chromium engine with native C++ integration, and its network stack is stock Chromium, untouched. The HTTP/2 SETTINGS frame is Chrome's own implementation, not a library approximation, and every layer of a profile's connection (TLS handshake, HTTP/2 SETTINGS, priority behavior, WINDOW_UPDATE timing) comes from the same browser. Each profile is its own browser with its own cookies, storage and proxy.

Step 6: How Different Vendors Use HTTP/2 Signals

Bot-management vendors weight HTTP/2 fingerprinting differently. Knowing this helps site owners interpret their own challenge logs, and helps teams that test their own sites with automation understand why a script gets challenged. If a site forbids automated access, use its official API or access route.

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 has published research on this, so it is well documented.

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. See our guide on browser fingerprint testing for related detection vectors.

DataDome: Its own published material describes connection-level fingerprinting explicitly.

PerimeterX (now HUMAN): Historically more JavaScript-focused. Check HUMAN's own documentation for what its network-layer checks cover.

(Opinion: Akamai leans on network-layer signals the most right now, Cloudflare weights behavioral scoring more heavily, and DataDome sits in the middle. Others may weigh them differently.)

The practical implication: a raw HTTP client and a real browser look different at the HTTP/2 layer, and vendors that analyse it will notice. If you need data from a protected site, the right route is its official API or a data agreement.

Common Questions When Testing Your Own Setup

Why does my script get a challenge page on a site I own?

You're probably using an HTTP library whose HTTP/2 settings differ from any browser. Compare the SETTINGS frame to a browser baseline. If you run the site, you can also allow-list your own test traffic in your bot-management configuration.

Why is the result inconsistent between requests?

Some systems sample requests rather than checking every one, and rules change between tests. Treat a single capture as a snapshot, not a guarantee.

Why do I get a "bot detected" JSON response instead of HTML?

Several CDNs return a JSON error for programmatic clients. It usually means the client was classified as non-browser early, at the connection level.

Why does Playwright/Puppeteer behave differently locally and in production?

Local tests often come from one IP with no rate limits, while production traffic is judged on volume and reputation. Also check headed versus headless: POST /profiles/{id}/start starts a profile headless unless the body says {"headless": false}, and a headless session has no real window, so its screen and viewport follow headless defaults. Run the JustBrowser desktop app on a Windows or macOS machine.

Next Steps

HTTP/2 fingerprinting is one layer in the stack. If you test your own site or want to understand what bot management sees, these are the other layers worth knowing:

The JustBrowser trial runs 7 days (card required, and you're not charged if you cancel within them) with nothing held back — unlimited profiles, API access included. That's enough to inspect the network behavior yourself before you're charged. It's $9.99/mo after that, or $99.99/year.

The honest take: plenty of sites never look past JavaScript-level signals, and you may never need to think about HTTP/2. But for teams that run enterprise sites or test them, it is one of the more useful layers to understand, partly because it is invisible without a packet capture. Block pages rarely explain why a client was challenged, so a capture is the reliable way to see what your client sends.

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 client sets these values differently — Chrome uses an INITIAL_WINDOW_SIZE of 6291456, Firefox uses 131072, and Python's h2 library defaults to 65535. Combined with WINDOW_UPDATE behavior and priority pseudo-headers, a server can build a signature that identifies your client before any page content loads.

Why do HTTP libraries like requests or urllib look different from real browsers over HTTP/2?

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. A server sees the mismatch — the TLS handshake says Chrome but the HTTP/2 settings say python-h2 — and can treat the client as automated.

How does Akamai's bot management 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 an HTTP/2 signature contradicts the claimed user agent or TLS fingerprint, the mismatch becomes a signal. This happens at the connection level before any JavaScript executes, so page-level scripts can't change it.

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 Chrome sends. Every profile is its own browser, so its connection behaves like a normal browser connection rather than a library approximation.


Try JustBrowser

A multi-account browser for teams on a custom Chromium engine — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. Local REST API + CDP for Playwright, Puppeteer, Selenium. Windows x64 and macOS (Apple Silicon). $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

Tags
  • http2-fingerprinting
  • akamai-bot-detection
  • network-layer-detection
  • multi-account-browser
  • browser-automation
  • buildinpublic
  • saasstudio
  • aiworkforce
  • buildwithclaude

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