How to Parse a User-Agent String Into Browser, OS, and Device
A practical guide to parse a User-Agent string into browser, engine, OS, and device — why every browser claims Mozilla, real uses, and where client hints fit.
How to Parse a User-Agent String Into Browser, OS, and Device
The first time I had to debug a "device type" bug in production analytics, I copied one User-Agent string out of a log line and stared at it for a solid minute. It was 140 characters long, it named four different browsers, and somewhere inside that mess was the single fact I actually needed: this was an iPad, not a desktop Mac. The User-Agent string is one of the strangest little artifacts the web still ships on every request, and reading it correctly is a real skill.
This post walks through what a User-Agent string actually contains, why it is such a historical pile-up, the cases where you genuinely need to parse one, and where the industry is heading with client hints.
What a User-Agent String Actually Packs In
A User-Agent string is a single line of text the browser sends in the User-Agent HTTP header on every request. Into that one line it crams four separate things: the browser brand and version, the rendering engine, the operating system and its version, and often the device vendor and model. A parser's whole job is to pull those four facts back out of one comma-and-space-soup.
Here is a typical Chrome-on-Windows string:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Look at how many lies are in there. It opens with Mozilla/5.0 — a token left over from the 1990s when servers checked for "Mozilla" to decide whether a browser could handle frames. Every browser still claims to be Mozilla, so the token carries zero information. It says AppleWebKit/537.36 and Safari/537.36, even though this is Chrome and not Safari at all, because Chrome inherited those tokens to stay on the friendly code path of old server sniffers. It says like Gecko, which is Firefox's engine — also a compatibility nod, also a lie. The only honest piece is Chrome/124.0.0.0, and even the OS hides behind Windows NT 10.0, which today maps to either Windows 10 or Windows 11.
A parser cuts through that. From this exact string a good parser reports: browser Chrome 124, engine Blink, OS Windows 10/11, device desktop. Four clean fields out of one tangled line.
Why the String Is Such a Mess
None of this is an accident — it is thirty years of compatibility scar tissue. In the Netscape era, servers sniffed for "Mozilla" to gate advanced features. Internet Explorer wanted those features too, so it also claimed to be Mozilla. When Safari arrived it borrowed Mozilla and KHTML tokens; when Chrome arrived it borrowed Safari's tokens; when Edge moved to Chromium it borrowed Chrome's. Every new browser added its own real token to the end while keeping all the inherited ones, because dropping a token risked some old server serving it a broken page.
The practical consequence is that matching order is everything. Naive substring matching falls apart instantly:
- Searching for
Chromematches Chrome — and Edge, Opera, Samsung Internet, and every other Chromium browser, all of which carry aChrome/token. - Searching for
Safarimatches real Safari — and Chrome, and every WebKit browser.
The fix is to test the most specific signature first. Check Edg/ before Chrome/, check Chrome/ before Safari/, check Firefox-on-iOS before plain Safari. Get the order wrong and your Edge users all show up as Chrome, and your funnel reports are quietly wrong. This is also why version numbers are a trap: Safari's real version lives in the Version/ token, not in Safari/605 — that number is the engine build, and reading it makes Safari 17 look like Safari 605.
What You Actually Use a Parsed User-Agent For
Parsing a User-Agent is not academic. The three places I reach for it constantly:
Analytics correctness. This is the iPad case. Since iPadOS 13, an iPad in desktop mode sends a Macintosh string with a Mobile token. Read only "Macintosh" and you call it a desktop Mac; read only "Mobile" and you call it a phone. The correct answer — Macintosh plus Mobile equals tablet — is the single most common source of inflated "desktop" numbers. When a PM swears mobile conversions cratered, parsing a few real UA strings is how you prove the analytics vendor's parser is the bug, not your funnel.
Bug reproduction. A user pastes "the page looks broken" with a screenshot and a UA string from your support widget. Parse it and you get the exact browser, version, and engine — say, Samsung Internet 23 on Android 14, Blink engine. Now you test in Samsung Internet specifically, not generic Chrome, and you know which engine quirks to check instead of guessing from "Android Chrome-ish."
Bot detection. Your nginx log spikes 40k hits overnight. A parser that names crawlers — Googlebot, Bingbot, GPTBot, ClaudeBot, AhrefsBot, curl, Python-requests — tells you instantly whether the spike is a friendly indexer you keep or a no-name scraper you block. Naming the bot, not just flagging "bot", is what lets you write a useful robots.txt or rate-limit rule. If you are debugging the requests those bots hit, pairing this with an HTTP status code explorer helps you tell a 200-that-served-junk from a 429-that-should-have-fired.
A Worked Example, Field by Field
Take that Chrome-on-Windows string again and walk it left to right the way a parser does:
Mozilla/5.0— ignored. Pure legacy. Every browser has it.(Windows NT 10.0; Win64; x64)— the platform block.Windows NT 10.0resolves to Windows 10/11;Win64; x64confirms 64-bit. OS field done.AppleWebKit/537.36 (KHTML, like Gecko)— engine bait. A naive parser might call this Safari or Gecko. The parser holds off, because a more specific brand may appear later.Chrome/124.0.0.0— the real signal, and noEdg/orOPR/appeared before it, so the browser is genuinely Chrome 124. Because it is Chromium, the engine field is set to Blink (Blink forked from WebKit, so theAppleWebKittoken stays for compatibility).Safari/537.36— ignored. Inherited Safari token, already overridden by the Chrome match.
Final structured output: { browser: "Chrome", version: "124", engine: "Blink", os: "Windows", osVersion: "10/11", device: "desktop", bot: false }. You can run that yourself in the User-Agent parser — paste the string, hit "Parse my browser" to read your own, or copy the result as JSON straight into a bug report.
Where Client Hints Come In
The whole reason this is a mess is that one freeform string carries everything, so browsers are slowly moving off it. User-Agent Client Hints replace the guessing game with structured, opt-in headers: Sec-CH-UA lists brands and versions, Sec-CH-UA-Platform gives the OS, Sec-CH-UA-Mobile is a clean boolean. A server asks for exactly the fields it needs, and the browser answers with structured data instead of a string you have to reverse-engineer. Chromium also "reduced" the legacy User-Agent string, freezing the minor version and trimming OS detail to push developers toward hints.
So is parsing dead? Not for years. Client Hints are a Chromium initiative; Safari and Firefox do not send the full set, and your access logs are full of historical UA strings from every client that ever hit you. The pragmatic stance is both: read Client Hints when they are present, and keep a solid User-Agent parser for everything else — logs, older browsers, bots, and the long tail of clients that will never send a single Sec-CH-UA header. The freeform string is a relic, but relics on the web have a way of outliving everyone who predicted their death.
Made by Toolora · Updated 2026-06-13