Developer Tools

User-Agent Client Hints

User-Agent Client Hints (UA-CH) replace the sprawling User-Agent string with opt-in, structured HTTP headers. Learn how the request flow works, which hints are low- vs high-entropy, and why it is a Chromium-only mechanism.

By Inventive HQ Team

User-Agent Client Hints (UA-CH) are opt-in HTTP request headers — all prefixed Sec-CH-UA — that replace the single, sprawling User-Agent string with structured fields a site receives only when it asks for them. By default a Chromium browser sends just three low-entropy hints (Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform). Detailed values like the device model, full browser version, and CPU architecture are high-entropy and stay hidden until the server advertises interest with an Accept-CH response header. The design flips the old model: instead of every site passively receiving everything, sites must actively request the data they need, and that request is visible and auditable.

That is the summary an AI Overview will give you. Here is what it can't show you: the actual two-round-trip request flow, the exact split between which hints are free and which cost an opt-in, and the browser-support reality that quietly breaks any "just switch to Client Hints" plan. The diagrams, tables, and header samples below are the parts that matter when you are actually implementing detection.

The request flow: why UA-CH takes a round trip

The single most important thing to understand about UA-CH is that high-entropy hints are not on the first request. The browser has to be told the origin wants them, then it includes them on the next request. This is the mechanism the legacy User-Agent string never had, and it is why naive server code "sees nothing."

User-Agent Client Hints request/response handshake A browser sends low-entropy hints, the server replies with Accept-CH listing the high-entropy hints it wants, and the browser includes them on the next request. The UA-CH opt-in handshake Browser Chromium Server your origin

1. First request — low-entropy only Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform

2. Response — server opts in Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model

3. Next request — high-entropy included + Sec-CH-UA-Platform-Version, Sec-CH-UA-Model

If a hint has to be present on the very first navigation — say you serve different assets based on platform version at the CDN edge — add a Critical-CH header alongside Accept-CH. The browser will retry the initial request with the critical hints attached rather than making you wait for a second navigation.

Low-entropy vs high-entropy: what you get for free

The whole privacy argument rests on this split. Low-entropy hints reveal little about an individual and ship by default. High-entropy hints narrow a user down and require the opt-in above.

HeaderEntropy tierSent by default?Example value
Sec-CH-UALowYes"Chromium";v="126", "Not(A:Brand";v="24", "Google Chrome";v="126"
Sec-CH-UA-MobileLowYes?0
Sec-CH-UA-PlatformLowYes"Windows"
Sec-CH-UA-Platform-VersionHighNo (Accept-CH)"15.0.0"
Sec-CH-UA-ArchHighNo (Accept-CH)"x86"
Sec-CH-UA-BitnessHighNo (Accept-CH)"64"
Sec-CH-UA-ModelHighNo (Accept-CH)"Pixel 8"
Sec-CH-UA-Full-Version-ListHighNo (Accept-CH)"Chromium";v="126.0.6478.126", ...
Which should I use?Request only the high-entropy hints you actually branch on; keep the default three otherwise.

The values are Structured Field values (RFC 9651, which supersedes RFC 8941): booleans render as ?0/?1, strings are double-quoted, and lists are comma-separated. Parse them with a Structured Fields library, not a regex — that is a core reason the format exists.

Advertisement

GREASE: the fake brand that keeps the format honest

Look again at that Sec-CH-UA example: "Not(A:Brand";v="24". That entry is not a real browser. It is GREASE — a deliberately fake brand with randomized punctuation and version that Chromium injects so that servers cannot hard-code an exact match against the brand list.

How GREASE appears inside the Sec-CH-UA brand list The Sec-CH-UA header contains three brand entries, one of which is a randomized fake brand that changes between builds. Sec-CH-UA is an order-independent, GREASE-salted list "Chromium";v="126" real engine brand "Not(A:Brand";v="24" GREASE — fake, randomized "Google Chrome";v="126" real product brand

Parse by matching each brand you know — never assume position or reject unknowns. A parser that expects an exact string will break the next Chrome release.

The GREASE brand's name, punctuation, and version rotate between Chrome builds. The lesson for anyone writing detection code: iterate the list looking for the brands you care about, and tolerate entries you have never seen. This is the same anti-ossification trick TLS uses.

Reading Client Hints in JavaScript

Server-side you read the headers. Client-side you read navigator.userAgentData. The low-entropy fields are synchronous; the high-entropy fields come back as a Promise so the browser can gate them behind permission policy.

if (navigator.userAgentData) {
  // Low-entropy, available immediately
  console.log(navigator.userAgentData.brands);   // [{brand, version}, ...]
  console.log(navigator.userAgentData.mobile);    // boolean
  console.log(navigator.userAgentData.platform);  // "Windows"

  // High-entropy, async and opt-in
  const hints = await navigator.userAgentData.getHighEntropyValues([
    "platformVersion", "model", "architecture", "fullVersionList",
  ]);
  console.log(hints.platformVersion, hints.model);
} else {
  // Firefox / Safari / non-secure context: fall back to navigator.userAgent
}

The else branch is not optional. navigator.userAgentData is undefined in Firefox and Safari, and also undefined over plain HTTP because Client Hints only operate in secure contexts.

The catch nobody puts in the summary: it's Chromium-only

The reason "just migrate to Client Hints" is bad advice is browser support. UA-CH ships in Chrome, Edge, Opera, and other Chromium browsers. Mozilla publicly assessed the full proposal as "harmful," and Apple's WebKit has not implemented it — so in Firefox and Safari there are no Sec-CH-UA headers and no navigator.userAgentData at all.

That means for the foreseeable future you maintain two paths: parse Client Hints where they exist, and fall back to the (now version-frozen, "reduced") User-Agent string everywhere else. Chrome's own UA-reduction effort froze the minor version and generalized the platform in the legacy string, so the fallback is coarser than it used to be — but it is still the only signal cross-browser. If you are untangling real-world traffic, our User-Agent Parser handles both the legacy string and the newer hint structure client-side, so nothing you paste leaves the browser.

Does UA-CH actually help privacy?

Partially, and it is worth being precise. The privacy win is that detailed, individuating data is no longer broadcast to every third party by default, and the Accept-CH opt-in is observable — you can audit which origins are asking for high-entropy hints. But a site determined to fingerprint can simply request every high-entropy hint and rebuild most of what the old string leaked. UA-CH shrinks the passive, default attack surface and makes data collection legible; it does not make browser fingerprinting go away. Treat it as a structural improvement, not a privacy guarantee.

Bottom line

UA-CH is a genuine improvement in how browser detection works: structured headers, an explicit opt-in for sensitive data, GREASE to keep the format evolvable, and a clean JS API. But it is Chromium-only, high-entropy hints cost a round trip, and it does not eliminate fingerprinting. Build detection that reads hints where available and falls back to the User-Agent string everywhere else — and request only the hints you actually branch on.

Frequently Asked Questions

What are User-Agent Client Hints?

User-Agent Client Hints (UA-CH) are a set of HTTP request headers, all prefixed with Sec-CH-UA, that expose browser and platform information as structured, opt-in fields instead of the single free-text User-Agent string. The browser sends a few low-entropy hints by default and only reveals detailed ones (model, full version, CPU architecture) when the server explicitly asks for them via the Accept-CH response header.

What is the difference between the User-Agent string and Client Hints?

The User-Agent string is a single header sent on every request that packs browser, engine, OS, and version into one unstructured line every site receives whether it needs it or not. Client Hints split that data into individual, machine-readable headers that are sent only when a site opts in. UA-CH is push-nothing-by-default; the legacy string is push-everything-always.

Which Client Hints are sent by default?

Only the three low-entropy hints are sent automatically on every request: Sec-CH-UA (a GREASE-protected brand and major-version list), Sec-CH-UA-Mobile (a boolean, ?0 or ?1), and Sec-CH-UA-Platform (the OS name as a quoted string). Everything else — platform version, full version list, architecture, bitness, device model — is high-entropy and withheld until the server requests it.

How does a server request high-entropy Client Hints?

The server sends an Accept-CH response header listing the hints it wants, for example "Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model". The browser remembers that preference for the origin and includes those hints on subsequent requests. Because the first request never carries them, any hint that must be present on the very first navigation should also be advertised via the Critical-CH header.

Do Firefox and Safari support User-Agent Client Hints?

No. UA-CH is effectively a Chromium-only mechanism, supported in Chrome, Edge, Opera, and other Chromium-based browsers. Mozilla labeled the full proposal "harmful" and Apple's WebKit has not shipped it, so navigator. userAgentData is undefined in Firefox and Safari. Any detection strategy built on Client Hints still needs a User-Agent string fallback.

Are Client Hints better for privacy than the User-Agent string?

They reduce passive fingerprinting because detailed values are no longer broadcast to every site by default and high-entropy hints require an opt-in that is easier to observe and audit. But a site that requests every high-entropy hint can reconstruct nearly the same fingerprint the old string leaked. UA-CH shrinks the default attack surface; it does not make fingerprinting impossible.

What is GREASE in Sec-CH-UA?

GREASE (Generate Random Extensions And Sustain Interoperability) inserts a deliberately fake brand with random punctuation and a random version into the Sec-CH-UA list, such as "Not(A:Brand";v="24". It stops servers from hard-coding exact brand strings, forcing parsers to treat the list as order-independent and unknown-tolerant so the format can evolve without breaking sites.

How do I read Client Hints from JavaScript?

Use the navigator.userAgentData object. Its brands, mobile, and platform properties expose the low-entropy hints synchronously, while navigator.userAgentData.getHighEntropyValues(["platformVersion", "model", "architecture"]) returns a Promise resolving to the requested high-entropy values. Always check that navigator.userAgentData exists first, since it is undefined outside Chromium and outside secure (HTTPS) contexts.

Why does my Sec-CH-UA header never appear on HTTP sites?

Client Hints are only sent in secure contexts. Over plain HTTP the browser omits every Sec-CH-* header and ignores Accept-CH entirely, so UA-CH works only on HTTPS origins. The Sec- prefix also marks these as forbidden headers, meaning page JavaScript cannot forge them via fetch or XMLHttpRequest.

user-agent-parser