12k
All articles

Can You Still Trust the User Agent String?

See which user agent string fields remain reliable, what browsers freeze, and how Chromium User-Agent Client Hints and feature detection can replace fragile parsing.

OpenReplay Team
OpenReplay Team
Can You Still Trust the User Agent String?

Partly: the user agent string still reliably tells you the browser family, major version, mobile versus desktop, and OS family, but not the OS version, device model, CPU architecture or (in Chromium browsers) the minor browser version.

If you’ve found Android 10; K in your logs from a phone that clearly isn’t running Android 10, or Mac OS X 10_15_7 from an M-series MacBook, nothing is broken. Those values are placeholders, and browsers send them on purpose.

This article takes a current Chrome string apart and shows which fields are frozen in each engine. It then covers what replaces the string in Chromium and when parsing it is still a reasonable choice.

Key Takeaways

  • Every major browser still starts its User-Agent string with Mozilla/5.0, a compatibility token that says nothing about the browser sending it.
  • Chrome reports Windows NT 10.0; Win64; x64, Macintosh; Intel Mac OS X 10_15_7, X11; Linux x86_64 or Linux; Android 10; K whatever the real OS release, and its minor, build and patch numbers are always 0.0.0.
  • Starting with Safari 26, Safari on iOS, iPadOS and visionOS reports a frozen OS version. Safari on Mac has frozen the macOS version since 2017.
  • User-Agent Client Hints exist only in Chromium-based browsers. Firefox and Safari send no Sec-CH-UA-* headers.
  • Any client can send any User-Agent, so the header only filters out bots that announce themselves.

What Does a Chrome User Agent String Actually Say?

Only one segment of a current Chrome desktop UA string changes between releases: the major version. Everything else is a fixed compatibility token or a frozen platform value. This is Chrome 154 on Windows:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
SegmentWhat it claimsIs it true?
Mozilla/5.0A Mozilla-compatible browserMeaningless. Every browser sends it
(Windows NT 10.0; Win64; x64)Windows 10, 64-bit x86Frozen. Windows 11 and ARM machines send the same value
AppleWebKit/537.36 (KHTML, like Gecko)A WebKit engine descended from KHTML and similar to GeckoCompatibility residue. Chrome’s engine is Blink
Chrome/154.0.0.0Chrome 154.0.0.0The major version is real. 0.0.0 is a placeholder
Safari/537.36SafariNot Safari. It stays so that code sniffing for “Safari” keeps matching

MDN’s Firefox reference describes Mozilla/5.0 as a generic token that claims Mozilla compatibility, and nearly every browser sends it whatever engine it runs. The MDN User-Agent reference confirms that Blink-based browsers carry KHTML, like Gecko and Safari purely as compatibility tokens. In a Chrome string, AppleWebKit/537.36 (KHTML, like Gecko) and Safari/537.36 describe neither Chrome’s engine nor Safari. They are fixed tokens kept so that old sniffing code still matches. To see the same breakdown for your own string, paste it into the OpenReplay user agent parser.

Which User Agent Fields Are Frozen, and Which Still Hold?

All three engines have frozen the high-entropy parts of the string: OS version, device model and CPU architecture. Chromium also zeroes the minor browser version. The low-entropy parts still reflect the real browser. Chrome’s User-Agent Reduction plan began fixing minor, build and patch at 0.0.0 in Chrome 101 (2022). Since the deprecation trial that let sites keep the full string ended on September 23, 2023, every page load gets the reduced string. MDN’s UA reduction guide lists the fixed platform values, including Android 10; K on Android.

Chrome / EdgeFirefoxSafari
Browser family, major versionRealRealReal (Version/)
Mobile vs desktop, OS familyRealRealReal
OS versionFrozenCapped (macOS 10.15, Android 10)Frozen (macOS; iOS since 26)
Device modelK on AndroidNever sentNever sent
CPU architectureFrozenFrozenMac always “Intel”
Minor version0.0.0Not exposedReal (Version/)
UA Client HintsYesNoNo

Edge uses the same frozen tokens as Chrome and adds Edg/. Since Firefox 87, Firefox reports every macOS release from Big Sur onward as 10.15 and labels Apple Silicon Macs as Intel. Since Firefox for Android 122, it reports Android 10 whatever the real version. According to the WebKit Safari 26.0 release post, Safari on Mac has sent the same Intel Mac OS X 10_15_7 value since 2017. Starting with Safari 26 (September 2025), Safari on iOS, iPadOS and visionOS also reports a frozen iOS 18 version instead of the real one. Safari 26.0 sent 18_6. From Safari 26.2, WebKit pins the value to the last iOS 18 release, so current strings send 18_7. The Version/ token keeps updating with each release.

Chromium’s reduction covers Chrome on Windows, macOS, Linux, ChromeOS and Android. It does not cover Android WebView or Chrome for iOS. In practice:

  • Windows 10 and Windows 11 look identical.
  • Apple Silicon Macs report “Intel” in all three engines.
  • A Chrome for Android string with Android 10; K does not describe an Android 10 device. Every Chrome on Android sends that version and the placeholder model K.

Why Is Feature Detection Better Than Browser Detection?

A browser’s name tells you nothing reliable about what it can do. Feature detection checks the capability directly, and that was true before any string was frozen. A UA check breaks when the string lies, when a new browser ships the API, or when an old version lacks it. Baseline gives you a cross-browser picture of when a feature is safe to rely on. When it isn’t, a runtime check covers the gap:

// Brittle: guesses capability from a name
if (/Chrome\/\d+/.test(navigator.userAgent)) enableShareButton();

// Direct: asks the browser
if ('share' in navigator) enableShareButton();

How Do User-Agent Client Hints Work?

User-Agent Client Hints are a set of Sec-CH-UA-* request headers that Chromium-based browsers send in place of the detail the UA string no longer carries. Chrome and Edge send Sec-CH-UA, Sec-CH-UA-Mobile and Sec-CH-UA-Platform by default. Firefox and Safari send none of them. MDN classes Sec-CH-UA-Platform as a low-entropy hint, so the browser includes it without the server asking, unless a permission policy blocks it. Every other hint has to be requested.

To request hints, the server lists them in Accept-CH, and the browser includes them on later secure requests to that origin.

Accept-CH does nothing for the first request. To get a high-entropy hint on the very first request, the server names it in Critical-CH as well as Accept-CH. Instead of rendering that first response, the browser sends the request again, this time with the hint. The server should also add the hint to Vary, so caches store each version separately.

import https from 'node:https';
import { readFileSync } from 'node:fs';

https.createServer(
  { key: readFileSync('key.pem'), cert: readFileSync('cert.pem') },
  (req, res) => {
    const platform = req.headers['sec-ch-ua-platform'];           // default hint
    const version = req.headers['sec-ch-ua-platform-version'];    // opt-in only

    res.setHeader('Accept-CH', 'Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch');
    res.setHeader('Critical-CH', 'Sec-CH-UA-Platform-Version');
    res.setHeader('Vary', 'Sec-CH-UA-Platform-Version, Sec-CH-UA-Arch');
    res.setHeader('Content-Type', 'text/plain');
    res.end(`platform=${platform ?? 'n/a'} version=${version ?? 'n/a'}\n`);
  }
).listen(8443);

Node lowercases header names. The values arrive as quoted structured-field strings such as "Windows". In the browser, navigator.userAgentData.getHighEntropyValues() returns the same data without any header setup. MDN marks uaFullVersion as deprecated in favor of fullVersionList.

async function describeClient() {
  const uaData = navigator.userAgentData;
  if (!uaData) return { source: 'ua', ua: navigator.userAgent }; // Firefox, Safari
  const high = await uaData.getHighEntropyValues([
    'platformVersion', 'architecture', 'model', 'fullVersionList',
  ]);
  return { source: 'ua-ch', brands: uaData.brands, mobile: uaData.mobile, ...high };
}

When Is Parsing the User Agent String Still Reasonable?

Parsing is still reasonable when you only need the fields that hold, and when being wrong costs little.

Analytics buckets. User agent parsing is accurate for analytics buckets like “Chrome 154, desktop, Windows”. A “Windows 10” or “Android 10” bucket is not: it silently absorbs newer releases, so treat it as the OS family only. Check for Edg/ before Chrome/, and Chrome/ before Safari/, because each string contains the tokens that come after it in that list.

Bot filtering. RFC 9110 defines User-Agent as a header the client supplies, and nothing verifies it. Any client can send any value. The header catches crawlers that identify themselves and does nothing against bots that don’t.

Support tickets. When a user reports a bug, you usually need to know roughly what they were running, not their exact build. Session replay tools record the UA with each session, which is enough to see that a bug reproduces in Firefox on macOS but not in Chrome. Recording the result of your feature checks alongside it narrows things further.

Conclusion

You can trust the user agent string for browser family, major version, mobile versus desktop, and OS family. Treat everything else in it as a placeholder. To act on these findings, audit your code for any branch that reads an OS version, device model or minor version from the string. Replace UA-based capability checks with feature detection, and move genuine needs for platform detail to Client Hints, with a fallback for Firefox and Safari, which don’t send them.

FAQs

How do I tell Windows 11 from Windows 10 if the user agent says Windows NT 10.0?

Request the platformVersion client hint, either with navigator.userAgentData.getHighEntropyValues(['platformVersion']) or by sending Accept-CH: Sec-CH-UA-Platform-Version. Microsoft documents values from 1.0.0 to 10.0.0 as Windows 10 and 13.0.0 or higher as Windows 11, and its sample code treats a major version of 13 or higher as Windows 11. Firefox sends no Client Hints, so it cannot tell the two apart.

How long does a browser keep sending hints requested with Accept-CH?

Chrome saves each origin's Accept-CH preferences to disk, and since Chrome 103 they have no fixed expiry. They last until the user clears cookies or site data for that origin, and they are also cleared along with session cookies. A server can resend Accept-CH to replace the list, send an empty Accept-CH to stop all hints, or send Clear-Site-Data: 'clientHints'.

Why does the Sec-CH-UA header contain a brand like Not A;Brand?

It is a deliberately fake entry known as GREASE. Chromium adds an intentionally wrong brand with a low version number, and varies its punctuation and its position in the list. This forces servers to parse the header properly instead of matching a fixed string or a fixed list of brands. Parse Sec-CH-UA as a structured-field list, look for the brand you need, such as Chromium, Google Chrome or Microsoft Edge, and ignore any entry you do not recognize.

Why does my user agent parser label Chrome on iPhone as Safari?

Chrome for iOS sends the Mobile Safari user agent string with a CriOS/ token in place of Version/, so the string has no Chrome/ token. Firefox for iOS does the same thing with FxiOS/. A parser that only looks for Chrome/ and Firefox/ falls through to Safari, so check for CriOS/ and FxiOS/ first. Chromium's UA reduction does not cover Chrome for iOS.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.