All posts
May 18, 20266 min read

Reading a user-agent string: a field-by-field guide

Every HTTP request your browser sends carries a User-Agent header. It is a single line of text that is supposed to describe the client: the browser, its version, the rendering engine, the operating system, and sometimes the device. In practice it is one of the messiest, most historically loaded strings on the web. Once you know the pattern, you can read almost any of them by eye.

Here is a typical desktop Chrome string:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36

That one line claims to be Mozilla, AppleWebKit, KHTML, Gecko, Chrome, and Safari all at once. None of that is a bug. It is the accumulated residue of three decades of compatibility decisions.

The general shape

A user-agent string is loosely structured as a list of products and comments:

Product/Version (comment) Product/Version (comment) ...

A product token is a name and a version separated by a slash. A comment is anything inside parentheses, usually platform details. Browsers chain several of these together, so the string reads left to right as a series of claims about what the client is and what it is compatible with.

Reading the example token by token

Take the Chrome string above and split it.

Mozilla/5.0

This is the prefix on essentially every browser today, including ones with no relationship to Mozilla. In the 1990s, servers sniffed for Mozilla to decide whether to send frames and modern HTML. Netscape was Mozilla, so browsers that wanted the good content pretended to be Mozilla too. The version froze at 5.0 and never moved. It now means nothing except "I am a web browser."

(Windows NT 10.0; Win64; x64)

This comment block holds the platform. Windows NT 10.0 is the OS version (Windows 10 and 11 both report 10.0). Win64 and x64 describe the architecture. On a Mac you would see something like Macintosh; Intel Mac OS X 10_15_7, and on Android something like Linux; Android 14; Pixel 8.

AppleWebKit/537.36 (KHTML, like Gecko)

This is the engine claim, and it is layered fiction. WebKit was forked from KHTML, the engine of the KDE Konqueror browser, so it credits KHTML, like Gecko to capture sites that sniffed for Gecko, Firefox's engine. Chrome later forked WebKit into Blink but kept the AppleWebKit/537.36 token verbatim so that nothing sniffing for WebKit would break.

Chrome/124.0.0.0

Finally, the real browser and its version. This is the token you usually care about. Note that the patch components are often zeroed out now to reduce fingerprinting surface.

Safari/537.36

One more compatibility tail. Chrome appends Safari because early mobile sites only served their good layout to Safari on WebKit, so Chrome claimed Safari too.

Engine versus browser

A common mistake is treating the browser name and the engine as the same thing. They are not.

  • The browser is the application: Chrome, Edge, Firefox, Safari, Brave.
  • The engine renders the page: Blink (Chrome, Edge, Brave, Opera), Gecko (Firefox), WebKit (Safari).

Edge, Brave, and Opera all run Blink and all carry a Chrome/ token, then add their own token (Edg/, OPR/) further along. If you only read the first browser-like token, you will label every Chromium browser as Chrome. The engine token tells you how a page will actually render, which is often what you really need for a layout bug. Tools like a JSON formatter or a regex tester behave identically across engines, but rendering and CSS quirks do not, so the engine matters when you reproduce a visual report.

Why the lies persist

Every "lie" in a user-agent string exists because removing it would break some site that sniffed for the old value. The string is append only by social contract. Vendors add new tokens but rarely delete old ones, because somewhere a server still keys off Mozilla or Safari or like Gecko. The result is a string that is mostly archaeology with a few useful facts buried inside.

Why UA sniffing is fragile

Parsing the user-agent to decide what code to run is brittle for several reasons:

  • The string is trivial to spoof. Any client can send any value, so it is not a security signal.
  • New browser versions and devices appear constantly, so a parser written today drifts out of date.
  • Vendors deliberately freeze and reduce the string to fight fingerprinting, so detail you relied on can vanish.

Two better approaches:

  1. Feature detection. Test whether the capability you need exists (if ('share' in navigator)) rather than guessing from the browser name. This is correct by construction and survives any UA change.
  2. Client Hints. The Sec-CH-UA family of request headers lets a server ask for specific, structured pieces (brand, platform, mobile flag) instead of parsing a freeform blob. They are opt in and far cleaner to read.

Use the user-agent for analytics, rough device buckets, and reproducing reports. Do not use it to gate features or trust a client.

Parse one without leaving the browser

When you need to break down a real string, paste it into the User-Agent Parser. It splits out browser, engine, OS, and device so you do not have to remember which token is fiction. Like every tool here, it runs entirely client side: the string you paste never leaves your machine, which matters because user-agent values can carry identifying detail. If you are decoding a session token alongside it, the JWT decoder works the same way, locally and with nothing uploaded.