How Accept, Accept-Language, and Accept-Encoding values and order fingerprint a browser before JavaScript runs — and why disabling scripts doesn't stop it.
Block every script, disable cookies, and browse with NoScript locked down — and a server can still fingerprint you. Every HTTP request carries a set of headers describing what your browser accepts, in what language, and in what encoding, and those headers arrive before a single line of JavaScript executes. Their values, their order, and which ones show up at all vary by browser in ways that are stable, hard to fake convincingly, and readable by any server on the very first request. This is fingerprinting that needs no canvas, no WebGL, no navigator object — just the request line itself.
Key Takeaways
- Accept, Accept-Language, and Accept-Encoding carry real entropy. Their values reflect installed language preferences and codec support, and combined they narrow down a visitor the same way canvas or WebGL does — just without any script running.
- Header order is itself a signal, independent of values. Each browser's networking stack emits headers in a fixed sequence; that sequence differs between Chrome, Firefox, and Safari and rarely changes across requests from the same install.
- None of this requires JavaScript. A server sees the full header set on the very first request — before HTML is even parsed, let alone before a script tag runs — so JS-blocking and NoScript don't touch it.
- It sits below Client Hints, not instead of them. User-Agent Client Hints add a second HTTP-layer signal on top of the always-sent Accept-family headers — a few low-entropy hints by default, the detailed ones only when a server asks via
Accept-CH. Both fire before JavaScript does. - It combines with the TLS and TCP layers underneath it. TLS fingerprinting reads the encrypted handshake beneath these headers, so a server checking both layers gets a request-level signature with no reliance on the page ever loading.
The Request Line Is Already a Fingerprint
Before a browser renders anything, it has already sent an HTTP request carrying a handful of headers whose entire job is content negotiation: telling the server what the browser can display, in which language, and how it prefers the response compressed. Over HTTPS that request is encrypted in transit, but the server terminating the connection reads it in the clear. A typical Chrome request includes something like:
GET / HTTP/1.1
Host: example.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br, zstd
Over HTTP/2 and HTTP/3 the same information travels as binary compressed fields with lowercase names, and Host is replaced by the :authority pseudo-header — the content-negotiation semantics are identical, and so is the fact that they arrive before any script does. (The framing layer adds signal of its own; see HTTP/2 fingerprinting.)
Nothing here requires script execution — this is the request that fetches the HTML document itself. A server logging these three headers already has more to work with than most people assume: the Accept family — Accept, Accept-Language, and Accept-Encoding — isn't boilerplate, it's negotiated per-browser and per-configuration, and RFC 9110 — the current HTTP semantics standard — defines the quality-value (q=) syntax that lets each browser express its own preference ranking, which is precisely where the variation comes from.
What Each Header Actually Reveals
Accept-Language is the most direct signal. It lists the languages the user has configured, in preference order, with q values expressing relative weight. Multi-language users — someone with en-US,en;q=0.9,fr;q=0.8,de;q=0.7 — expose a specific, comparatively rare combination; the exact q weighting scheme also differs slightly by browser and OS locale settings, adding a layer that isn't just "which languages" but "how this particular stack formats a language list."
One caveat about how much that's worth on its own: a visitor sending a plain en-US,en;q=0.9 shares that value with an enormous crowd, so the header contributes very little identifying information by itself. The entropy lives in the unusual cases — a three- or four-language list, a rare regional variant, a language that doesn't match the exit IP's country — and in the combination with everything else in the request. That's the same entropy and anonymity-set math that governs the JavaScript-side signals.
Accept-Encoding lists which compression algorithms the client will decode: gzip, deflate, br (Brotli), and increasingly zstd. Support for newer algorithms rolls out browser-by-browser and version-by-version, so the exact set — and the order in which they're listed — narrows down not just "a Chromium browser" but roughly which release range sent the request.
Accept describes which content types and subtypes the browser will render, weighted by preference — and it directly reveals things like AVIF or WebP image support, which correlates tightly with browser family and version.
Individually, each header narrows the crowd a little. Read together, the combination behaves exactly like the entropy math behind canvas and WebGL fingerprints: independent signals whose bits of identifying information add up.
Header Order and Presence: A Signature Beneath the Values
The values aren't the only thing that varies — the set and order of headers a browser sends is itself close to a fixed signature. Each browser's HTTP client library assembles headers in the sequence its networking code happens to build them in, and that sequence is consistent across requests from the same browser and version, but differs from browser to browser. A request claiming to be Chrome that arrives with a header order matching Python's requests library or curl is an immediate mismatch — the same logic that makes TLS ClientHello ordering a fingerprint applies one layer up, at the HTTP framing level, to plain header ordering. And because HTTP/2 and HTTP/3 impose their own pseudo-header conventions on top, the framing layer itself carries additional signal that a raw HTTP/1.1 proxy or scripting library often gets subtly wrong.
This is also why the header set matters as much as any single value: a real browser sends a predictable, complete cluster of headers on every navigation; a scripted client that only sets User-Agent and forgets Accept-Language or Accept-Encoding — or sends them in an order no real browser uses — stands out precisely because of what's missing or out of place, not because any individual value looks wrong.
Why This Survives Disabled JavaScript
The DOM-level fingerprinting signals covered elsewhere on this site — canvas, WebGL, audio, fonts, permissions — each need a script to run and call an API. HTTP headers require nothing of the kind. They're attached by the browser's network stack to the very first request for the very first byte of HTML, which means:
- Disabling JavaScript entirely (NoScript, a text-only browser) has zero effect — the headers still go out.
- Blocking third-party scripts or trackers doesn't help either, since the first-party server that serves the page sees the headers directly.
- Even a request that never renders a page — a HEAD request, a redirect that's never followed to completion — still carries the full header set.
This is the same property that makes TLS and TCP/IP fingerprinting resilient to script-blocking: any signal that lives at the network or protocol layer, below the DOM and below the JavaScript engine, is simply out of reach of privacy tools that operate by disabling scripts.
How It Stacks With Client Hints and TLS
HTTP-only fingerprinting isn't a replacement for the other network-layer signals — it's the layer they all sit on top of, or alongside:
| Layer | Signal | Requires JS? | Requires HTTPS? |
|---|---|---|---|
| TCP/IP | Window size, TTL, options ordering | No | No |
| TLS handshake | JA3/JA4 from the ClientHello | No | Yes |
| Plain HTTP headers | Accept/Accept-Language/Accept-Encoding values + order | No | No |
| Client Hints | Sec-CH-UA-* low-entropy defaults | No | Yes |
| Client Hints (high-entropy) | Full version, architecture, model | No (but needs Accept-CH opt-in) | Yes |
| Browser APIs | Canvas, WebGL, fonts, audio | Yes | No |
A server that reads down this whole stack gets a signature assembled entirely before — and independently of — anything JavaScript-based, then layers the DOM-level signals on top for visitors who do execute scripts. The Accept-family headers and Client Hints are both HTTP-layer and both JS-independent; the difference is that Client Hints are Chromium-specific and require server opt-in for the richer values, while the plain Accept headers arrive from every browser, on every request, with no negotiation needed.
Check Your Own Headers
You can look at both sides of this line yourself. BrowserInsight's fingerprint check reports the language list your browser exposes to scripts (navigator.languages) next to the rest of the JavaScript-side signals, and it also reports the one network-layer signal a page cannot compute for itself: the TLS handshake your connection was observed making, including JA3/JA4, TLS version and ClientHello size. That handshake card is the layer sitting directly beneath the headers described here — same "before any script runs" property, read by the server rather than by the page.
To compare that with the header layer specifically, note the language list the tool shows and then check what your browser actually sends in Accept-Language using any request-echo endpoint or your browser's own DevTools network panel (open the Network tab, click the document request, and read the request headers). The two often differ in detail — navigator.languages and Accept-Language are derived from the same preferences but formatted independently — and that difference is precisely the kind of inconsistency a detection system looks for. Change your language settings, or repeat it in a different browser, and both sides shift in ways you can watch.
Frequently Asked Questions
Does using a VPN change any of these headers?
No. A VPN changes your IP address, not your browser's networking stack — Accept, Accept-Language, and Accept-Encoding are generated by the browser itself and pass through a VPN tunnel unchanged. If your Accept-Language still says en-US while your VPN exit IP geolocates to another country, that mismatch is its own signal, similar to the timezone-vs-IP mismatch other tools check for.
Can I spoof these headers to look like a different browser?
The values, yes — any HTTP client library lets you set arbitrary headers. Matching a real browser's exact header set and order consistently, on every request, including how HTTP/2 frames them, is much harder, which is why mismatched or incomplete header sets are a common tell for scripted traffic, not just spoofed values.
Is this the same thing as TLS fingerprinting?
Related but distinct. TLS fingerprinting reads the handshake that happens before any HTTP request is even sent; HTTP header fingerprinting reads the request that follows once the handshake completes — encrypted in transit, but fully readable by the server that terminates the connection. Both are JavaScript-independent, and servers increasingly check both together, since a request can pass one check and fail the other.
Does disabling cookies stop this kind of fingerprinting?
No — cookies are unrelated. Header-based fingerprinting doesn't store anything on your device; it reads what your browser sends by default on every request, the same way browser fingerprinting in general doesn't rely on stored data either.
Conclusion
Most fingerprinting discussions center on JavaScript-driven signals — canvas hashes, WebGL renderer strings, font enumeration — because those are the richest, highest-entropy sources available to a tracker. But a meaningful signature exists one layer down, in the plain HTTP headers every browser sends before any script has a chance to run: what it accepts, in what language, with what encoding, and in what order. Disabling JavaScript closes off the DOM-level signals; it does nothing to the request line itself. Combined with TLS and TCP fingerprints below it and Client Hints alongside it, this header-only layer means there's no such thing as a truly signal-free request — only requests where nobody bothered to look below the page.
Recommended Reading:


