The Akamai HTTP/2 fingerprint is a short string made from four things a client sends when an HTTP/2 connection opens: its SETTINGS values in order, its WINDOW_UPDATE increment, its PRIORITY frames and the order of its pseudo-headers. The server computes it before it serves any content, so no script can touch it. We compared our engine's against Google Chrome's and found nothing to separate them.
Where the string comes from
Akamai's threat research team presented the method at Black Hat Europe 2017, in a white paper titled "Passive Fingerprinting of HTTP/2 Clients". HTTP/2 leaves a client a lot of small decisions. Which settings to announce, and with what values. How much extra window to grant. Whether to build a priority tree. What order the pseudo-headers go in. A browser's network code makes each of those decisions once, when somebody writes it, and then repeats them on every connection it ever opens.
The paper's title says "passive", and that word carries the method. Nothing is probed or requested. The server just keeps the frames the client was sending anyway.
The Akamai HTTP/2 fingerprint explained, field by field
Written out, the fingerprint is four parts joined by a pipe:
SETTINGS|WINDOW_UPDATE|PRIORITY|PSEUDO_HEADER_ORDER
To show the shape, here is one with made-up values: 1:4096;3:100;4:65535|0|0|m,p,s,a. Each part reads like this.
SETTINGS
The first frame a client sends after the connection preface. Each entry is a setting's numeric id and the value announced, in the order sent, separated by semicolons. The ids are defined by the HTTP/2 specification: 1 is the header table size, 2 is server push, 3 is the maximum concurrent streams, 4 is the initial window size, 5 is the maximum frame size and 6 is the maximum header list size, and later RFCs added 8, for the extended CONNECT protocol, and 9, for turning off the old priority scheme. Which ids appear, their order and their values all count. A client that leaves a setting out is saying something too.
WINDOW_UPDATE
HTTP/2 starts every connection with a small flow-control window, and most clients immediately send a WINDOW_UPDATE on stream 0 to make it larger. The fingerprint records the increment, or 0 if none was sent. It is one number, and different stacks pick quite different ones.
PRIORITY
Some clients send PRIORITY frames at the start to build a dependency tree of streams. Each is written as stream id, exclusive bit, the stream it depends on and its weight, with frames separated by commas, and 0 if there were none.
Pseudo-header order
Every HTTP/2 request opens with four pseudo-headers, :method, :authority, :scheme and :path. The protocol does not fix their order, so each implementation has its own, written as the first letters: m,a,s,p or some other arrangement.
Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.
Mosaic is a desktop app
Email yourself the link and open it on your computer.
How it works: Machines that exist
Why it is hard to fake from outside the browser
None of the four fields comes from the page. They come from the HTTP/2 stack, and that cuts both ways. Chrome can't send the wrong ones, since they're simply what its network code does. Now take a script that puts a Chrome User-Agent on an ordinary HTTP library: out go the library's SETTINGS, the library's window increment, the library's pseudo-header order. The server is looking at a "Chrome" whose opening frames were written by somebody else, and it knows that before it has sent one byte of HTML. To fake it you would have to rewrite the frames themselves, every value in the right order, for the exact Chrome version the User-Agent names.
What we measured on our own engine
Tesseract Chrome is stock Chromium source with our patch series on top. We went through the series file by file: most of the patches live in Blink, the rest in the browser process, V8 and our own fingerprint component, and not one of them opens a file under net/. On paper, then, our HTTP/2 fingerprint is Chrome's. We measured it anyway. "On paper" is how a suspect gets waved through.
It came up while we were chasing an IPHey score, where our engine got a worse verdict than branded Chrome on the same Mac. Ruling HTTP/2 in or out cost one probe. We wrote a small node http2 server with a self-signed certificate, launched both browsers at it with --ignore-certificate-errors, and printed what arrived from each. Same machine, same minute, nothing rebuilt.
| What we compared | Our engine vs Google Chrome |
|---|---|
| SETTINGS frame (header table size, push, max concurrent streams, initial window, max frame, max header list, enable connect protocol) | same |
| Header order | same |
| ALPN | same |
To be exact about scope: the probe printed the settings, the header order and ALPN. It did not print the WINDOW_UPDATE increment or the PRIORITY frames as rows of their own. Those come out of the same untouched code, so we had no reason to expect a difference there, but we didn't print them and we won't claim them. The IPHey score had its causes somewhere else. HTTP/2 went on the list next to the TLS handshake: identical, and not the tell. (The handshake probe, which took about a minute, is in reading a browser's ClientHello with a one-minute listener.)
Why we don't vary it per profile
Some anti-detect browsers sell control over network fingerprints as a feature. We kept Chrome's TLS and HTTP/2 fingerprints on purpose. Every copy of one Chrome version sends identical frames, which is what makes the crowd. Give one profile its own string and it is the only browser anywhere sending it. Let the string drift away from the version in the User-Agent and you have built the precise contradiction Akamai's method was written to catch. The same argument for the handshake is in why our engine has no TLS fingerprint setting, and why Chrome's JA3 changes between connections anyway is in why Chrome's JA3 hash changes on every connection.






