How User-Agent Client Hints (UA-CH) work: low- and high-entropy hint tiers, Accept-CH header negotiation, and what the API means for browser fingerprinting.
The User-Agent string has been part of the web since the early 1990s — a single header that announces your browser, version, engine, operating system, and device class all at once. Over the decades that string grew unwieldy: it carries tokens like Mozilla/5.0 that echo browsers long dead, and enough platform detail to serve as a passive fingerprinting signal on every request. User-Agent Client Hints (UA-CH) are Chromium's structured replacement. Rather than broadcasting everything up front, the API operates on a two-tier disclosure model: a small default set of low-entropy hints goes out on every request, and a richer high-entropy tier is only released when a server explicitly asks for it.
Key Takeaways
- Chromium browsers send three low-entropy hints (brand list, mobile flag, platform name) on every HTTPS request by default — no server opt-in needed.
- High-entropy details (architecture, full version, OS version, device model) require the server to send an
Accept-CHresponse header first. - The JavaScript counterpart is
navigator.userAgentData— a Chromium-only API; Firefox and Safari returnundefined. - Chromium is gradually freezing the legacy
User-Agentstring to a low-detail stub while expanding UA-CH as the primary identity channel. - For fingerprinting and spoofing detection, the presence of
navigator.userAgentDataalone reliably identifies Chromium — and high-entropy values, when granted, expose more specific browser identity than the old UA string ever did.
Why the legacy User-Agent needed replacing
The original UA string was designed for browser sniffing — servers would read it, decide which layout engine they were dealing with, and send appropriate content. It was never designed for privacy. A typical Chromium UA string looks like this:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
That single header passively leaks the OS name and version, CPU architecture, platform class, browser brand, and a rough version number — on every request, without any server asking. It also became a compatibility trap: sites started branching on specific substrings, so browsers couldn't change the format without breaking those sites. The string is a maintenance problem as much as a privacy one. UA-CH addresses both: it freezes the legacy string to a stub while providing a clean, versioned API through which servers can ask for exactly what they need.
The two tiers: low-entropy and high-entropy hints
The UA-CH model divides browser information into two tiers based on how much they narrow down a user's identity.
Low-entropy hints (sent by default)
Chromium attaches three Sec-CH-UA-* headers to every cross-origin HTTPS request without any server prompt:
| Header | Example value | What it reveals |
|---|---|---|
Sec-CH-UA | "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99" | Browser brand(s) and major version |
Sec-CH-UA-Mobile | ?0 | Whether the device is considered mobile (?1) or not (?0) |
Sec-CH-UA-Platform | "Windows" | High-level OS name only |
These are low-entropy because the values are shared by a large population: knowing someone is on Chrome 124 for Windows describes millions of users, not a handful.
High-entropy hints (require Accept-CH)
Higher-resolution details — the kind that could identify a specific device build — are off by default. A server must request them explicitly:
| Header | Example value | What it reveals |
|---|---|---|
Sec-CH-UA-Arch | "x86" | CPU architecture |
Sec-CH-UA-Bitness | "64" | 32- or 64-bit platform |
Sec-CH-UA-Full-Version-List | "Google Chrome";v="124.0.6367.60" | Full version strings for each brand |
Sec-CH-UA-Model | "Pixel 7" | Device model (mainly meaningful on mobile) |
Sec-CH-UA-Platform-Version | "10.0.0" | OS version |
Accept-CH negotiation
The protocol for requesting high-entropy hints is straightforward. On first visit, the server responds with an Accept-CH header listing the hint names it wants:
HTTP/1.1 200 OK
Accept-CH: Sec-CH-UA-Full-Version-List, Sec-CH-UA-Arch, Sec-CH-UA-Platform-Version
On subsequent requests (and on page reload), the browser includes those hints:
GET /page HTTP/1.1
Sec-CH-UA: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Platform: "Windows"
Sec-CH-UA-Full-Version-List: "Chromium";v="124.0.6367.60", "Google Chrome";v="124.0.6367.60", "Not-A.Brand";v="99.0.0.0"
Sec-CH-UA-Arch: "x86"
Sec-CH-UA-Platform-Version: "10.0.0"
The first page load is intentionally information-sparse: without a prior opt-in, the server sees only the three low-entropy defaults.
The JavaScript API: navigator.userAgentData
The same data is available client-side through navigator.userAgentData. The API only exists in Chromium — Firefox and Safari return undefined for it — so its presence is itself a reliable engine signal.
Low-entropy data is synchronous:
const uaData = navigator.userAgentData;
console.log(uaData.brands); // [{brand: "Chromium", version: "124"}, ...]
console.log(uaData.mobile); // false
console.log(uaData.platform); // "Windows"
High-entropy data is asynchronous, requiring an explicit request that mirrors the HTTP Accept-CH model:
navigator.userAgentData
.getHighEntropyValues(["architecture", "platformVersion", "fullVersionList"])
.then(ua => {
console.log(ua.architecture); // "x86"
console.log(ua.platformVersion); // "10.0.0"
console.log(ua.fullVersionList); // [{brand: "Google Chrome", version: "124.0.6367.60"}, ...]
});
Calling getHighEntropyValues() does not trigger a user-visible permission prompt. It does give sites a traceable, explicit mechanism for pulling detailed identity information, as opposed to silently reading a broadcast string. Cross-origin iframes can be blocked from high-entropy hints via Permissions-Policy.
UA-CH vs the legacy User-Agent
| Property | Legacy User-Agent | User-Agent Client Hints |
|---|---|---|
| Browser support | All browsers | Chromium only |
| Disclosure model | Everything broadcast passively | Low-entropy default + high-entropy opt-in |
| Format | Unstructured freeform string | Structured key-value pairs |
| Spoofability | Trivial (one string to change) | Per-field; harder to fake consistently |
| Privacy intent | None (historical artifact) | Progressive disclosure designed in |
| Trajectory | Gradually freezing to a stub | Expanding; primary Chromium identity channel |
Chromium has been progressively reducing detail in the legacy UA string. Since Chrome 101 the minor OS version has been frozen to 0.0.0, and the browser minor version in the UA string is reduced. The trajectory is toward a stub that conveys little beyond "this is a Chromium browser" — all specifics migrating to UA-CH.
Fingerprinting implications
UA-CH changes the fingerprinting landscape in ways that cut both ways.
Presence of navigator.userAgentData is a reliable engine detector. Firefox and Safari don't expose this API. A page that checks navigator.userAgentData !== undefined instantly identifies Chromium — more reliably than parsing the UA string, which can be spoofed with a single extension. If a spoofed UA claims to be Firefox but navigator.userAgentData exists and reports Chromium brands, the contradiction is immediately visible. This is one of the key signals covered in detecting user-agent spoofing.
High-entropy hints can be more specific than the old UA string. The frozen legacy UA no longer exposes the full minor browser version; Sec-CH-UA-Full-Version-List does. Combined with Sec-CH-UA-Arch and Sec-CH-UA-Platform-Version, a server that requests the full set gets a richer platform fingerprint than the passive broadcast ever provided — but only with the browser's cooperation and only from servers that explicitly request it.
Frozen UA string ≠ better privacy by itself. Reducing the UA string removes one passive signal, but it does nothing about the many other signals that make up a browser fingerprint: canvas rendering output, WebGL renderer strings, screen metrics, audio processing, installed fonts. UA-CH improves server-side information hygiene without shrinking the overall fingerprint surface.
UA-CH is server-controlled, not user-controlled. There is no browser UI to refuse a specific getHighEntropyValues() call from first-party JavaScript. Brave ships UA-CH spoofing as part of its fingerprint randomization; other Chromium-based browsers do not. Users who need to limit disclosure must rely on the browser's anti-fingerprinting mode.
Test it yourself
Open your browser's DevTools console and run navigator.userAgentData to see your low-entropy values. Then run:
navigator.userAgentData?.getHighEntropyValues(
["architecture", "model", "platformVersion", "fullVersionList"]
)
In Firefox or Safari the API returns undefined. In Chromium it resolves to an object with your actual platform details. BrowserInsight's fingerprint check exposes your full UA-CH data alongside the rest of your fingerprint, so you can see how all the identity signals combine.
Frequently Asked Questions
Does UA-CH replace the User-Agent string entirely?
Not yet, and not for non-Chromium browsers. Firefox and Safari still only send the traditional User-Agent header. For Chromium, UA-CH is the preferred channel and the legacy UA is being frozen, but both coexist in current browser versions. Sites that need broad compatibility still read User-Agent as the fallback.
Can I spoof my UA-CH values?
Not through normal browser settings. Chrome's DevTools device emulation overrides the UA string but does not override navigator.userAgentData, which is why device emulation is detectable. Brave's fingerprint randomization mode does override UA-CH values, but doing it consistently across both the HTTP headers and the JavaScript API requires coordinated patching at the browser level.
Does UA-CH work over HTTP?
No. The Accept-CH negotiation and Sec-CH-UA-* headers only apply over HTTPS (and localhost). Plain HTTP connections do not send UA-CH headers.
Does Firefox support UA-CH?
Firefox does not implement UA-CH. The Mozilla team has raised concerns that the high-entropy tier could make fingerprinting easier by providing finer-grained data to any first-party script that calls getHighEntropyValues(). Firefox continues to use the legacy User-Agent header.
Conclusion
User-Agent Client Hints represent a deliberate architectural shift: from a single passive broadcast that leaks platform detail whether or not anyone asks, to a tiered system where low-entropy facts go out by default and high-entropy specifics require an opt-in. For developers, the structured API is easier to parse and version than the freeform UA string. For fingerprinting and detection work, it introduces a definitive Chromium-vs-non-Chromium signal while providing a richer, explicitly-requested identity channel for servers that need it. The frozen legacy UA string reduces one passive vector; what replaces it is an active negotiation between browser and server.
Recommended Reading:


