What a user agent string is
Every HTTP request a browser makes carries a User-Agent header: a single line of free text in
which the client announces what it claims to be. A modern desktop Chrome sends something close to
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/120.0.0.0 Safari/537.36, and almost none of that is true. It is not Mozilla, it is not
AppleWebKit in the sense the token implies, it is not KHTML, and it is not Safari. The only informative
token is Chrome/120.0.0.0, and even its last three components are deliberately zeroed.
The reason is thirty years of defensive imitation. In the mid-1990s some servers sent frame-capable markup
only to clients identifying as Mozilla, so Internet Explorer began calling itself
Mozilla/4.0 (compatible; MSIE 5.0; ...). Every subsequent browser inherited the habit,
because a browser that told the truth was served a worse page than one that lied. Safari added WebKit while
claiming KHTML; Chrome added its own token while claiming both WebKit and Safari; Edge, once it moved to
Chromium, claims Chrome as well and distinguishes itself only by a trailing Edg/. The string
grew by accretion and nothing was ever removed, because removing a token was the one change guaranteed to
break someone's sniffing code.
The practical shape of that history is the parsing rule everything below depends on: order
matters, and specificity wins. You must check for Edge before Chrome, for Chrome before Safari,
for Opera's OPR/ before Chrome, and for the various iOS browsers — which are all obliged to
use the system WebKit and so all look like Safari — before Safari itself. A parser that tests in the wrong
order reports Chrome for every Edge user and Safari for everything on an iPhone.
How to read the parts
| Token | What it usually means |
|---|---|
Mozilla/5.0 | Nothing. A compatibility prefix carried by essentially every browser. |
(Windows NT 10.0; Win64; x64) | The platform comment. OS family, kernel version and CPU architecture. NT 10.0 covers both Windows 10 and 11 — they are indistinguishable here. |
(Macintosh; Intel Mac OS X 10_15_7) | macOS. The version has been frozen at 10.15.7 for years regardless of the real one. |
(iPhone; CPU iPhone OS 17_1 like Mac OS X) | iOS, with underscores instead of dots. An iPad reports CPU OS rather than iPhone OS — and iPadOS in desktop mode reports Macintosh instead. |
(Linux; Android 13; Pixel 7) | Android, with the OS version and a device model that newer Chrome releases replace with a generic K. |
AppleWebKit/537.36 | Frozen. Present on Chrome, Edge, Opera and Safari alike and no longer distinguishes anything. |
(KHTML, like Gecko) | Pure archaeology. KHTML was the KDE engine WebKit forked from in 2001. |
Chrome/120.0.0.0 | Chromium version. The last three components are zeroed as an anti-fingerprinting measure. |
Edg/120.0.2210.91 | Edge. Deliberately not Edge/, which was the old EdgeHTML browser. |
Version/17.1 Safari/604.1 | Safari's real version is in Version/; the Safari/ number is a WebKit build. |
Firefox/121.0 with rv:121.0 | Gecko. The rv: value in the platform comment matches, except on Firefox for iOS, which sends FxiOS/ and runs WebKit. |
Mobile / Mobi | The most reliable phone signal available. An Android tablet is simply an Android UA without it. |
Non-browser clients are the pleasant exception: they are short, honest and easy to parse.
curl/8.4.0, python-requests/2.31.0, PostmanRuntime/7.36.0,
Go-http-client/2.0, okhttp/4.12.0 and axios/1.6.2 each say exactly
what they are. If you are working with these, the curl converter shows
which User-Agent a copied command is carrying, and the
API Tester lets you set one explicitly and see how a server's response changes.
Why this is unreliable, stated plainly
Every user agent parser, this one included, is a stack of ordered regular expressions maintained against a moving target. There is no grammar that identifies a browser unambiguously, no registry of legitimate values, and no verification of any kind. Three separate things make the result a guess rather than a fact.
The field is attacker-controlled and user-controlled. Anyone can send any string. Privacy extensions rotate or spoof it; developer tools change it with two clicks; scrapers copy a real Chrome string verbatim; bots pretending to be Googlebot are common enough that Google publishes reverse-DNS instructions for verifying the real one. Any decision with a security consequence that rests on this header is broken by construction — the same rule that applies to every other client-supplied HTTP header.
The information is deliberately being removed. Because a detailed UA is a strong
fingerprinting signal, browsers now reduce it. Chrome and Edge freeze the minor version components at zero
and generalise the platform version; Android device models are being replaced by a literal K;
macOS has reported 10_15_7 for years no matter what version is actually running; Safari on iOS
reports a coarse OS version. So the parser below can tell you the major browser version and very little
else with confidence, and that is by design rather than by accident.
The heuristics decay. Every new browser that imitates an existing one, and every vendor
that changes its format, silently breaks parsers written before it. Rules written for pre-Chromium Edge
misidentify current Edge; rules written before OPR/ misidentify Opera; anything that assumes
an iPad is not a Mac now misidentifies iPadOS in desktop mode. There is no version of this problem that
stays solved.
What follows from this is a short list of things UA parsing is genuinely good for: aggregate analytics, where being wrong about a small percentage does not change the conclusion; reproducing a reported bug, where you want to know roughly what the reporter was running; picking the right app-store link on a download page; and identifying bots and API clients in server logs, where the clients are honest anyway. It is not good for deciding which code path to run.
Feature detection, and Client Hints
The correct replacement for asking which browser is this is asking does this browser do the
thing I need. Feature detection — testing for the API, the CSS property or the format directly —
remains correct when a browser gains the feature, when a browser you have never heard of already has it,
and when a user spoofs their UA. Sniffing is wrong in all three cases. In practice that means checking
'IntersectionObserver' in window rather than a version number,
CSS.supports() rather than a vendor name, and letting
<picture> or content negotiation pick an image format rather than inferring it from a
browser string.
Where you genuinely need client identity — analytics, or serving a platform-appropriate binary — the
sanctioned mechanism is User-Agent Client Hints. Rather than one opaque line, the browser
exposes structured values: a brand-and-version list, the platform, and a mobile boolean. Low-entropy hints
arrive by default in the Sec-CH-UA, Sec-CH-UA-Mobile and
Sec-CH-UA-Platform request headers, and are readable in JavaScript through
navigator.userAgentData. Higher-entropy values — the full platform version, the device model,
the full browser version list — must be requested explicitly, either by asking for them in JavaScript or by
sending an Accept-CH response header, which makes the privacy cost of each one visible and
auditable.
The honest caveat is that Client Hints are, at the time of writing, a Chromium feature; Firefox and Safari
have not adopted them. So a real implementation reads navigator.userAgentData when it exists
and falls back to parsing the string when it does not — which is why this page shows both. The panel above
reports whatever your own browser exposes, so you can compare the structured answer with the parsed one
directly.
One more note for anyone building a server that consumes this. The brand list in Client Hints deliberately
includes a randomised nonsense brand — entries such as Not_A Brand with an arbitrary version
— precisely to break code that assumes a fixed list or reads a fixed index. Iterate the list and match the
brand you care about; never index into it. It is the same defensive instinct that produced the
Mozilla/5.0 prefix in the first place, applied deliberately this time.
Working with user agents in API requests
User-Agent matters on the request side too, and more than people expect. A great many APIs
require one and answer 403 Forbidden to requests without it —
GitHub's API is the best-known example, and the failure is confusing because the endpoint works perfectly
in a browser. Others rate-limit per user agent, or route requests differently based on it. When a script
fails against an endpoint that works interactively, an absent or default
User-Agent is worth ruling out early, alongside the usual
401 causes.
The convention for a client of your own is a descriptive product token with a contact point:
something like my-service/1.4 (+https://example.com/bot). That gives an operator who sees
unusual traffic in their logs a way to reach you instead of blocking you, and it is the difference between
being treated as a partner and being treated as a scraper. Note also that in a browser
User-Agent is a forbidden header name — fetch() will not let a page set it — so
any test involving a custom UA has to come from a non-browser client. That is one of several things the
CORS tester demonstrates directly, and one of the reasons our
API Tester issues requests from a server rather than from the page.
If you are parsing user agents out of access logs at any volume, use a maintained library rather than hand-written patterns — the browser-vendor churn described above is exactly the maintenance burden a shared regex database exists to absorb. This tool is for the one-off case: a single string from a bug report, a log line you are trying to attribute, or a check on what your own browser is announcing. For the wider request-debugging loop, REST API testing covers where this fits.
Frequently asked questions
Why does every browser's user agent claim to be Mozilla?
It is thirty years of accumulated compatibility lying. Early servers sent frames only to Mozilla, so
Internet Explorer called itself Mozilla/4.0 (compatible; MSIE ...). Every browser since has
inherited that prefix, and each new engine added the tokens of its predecessors to avoid being served a
degraded page: Chrome claims KHTML, like Gecko and Safari, Edge additionally
claims Chrome. The result is a string whose first two thirds are historical noise and whose
meaning lives entirely in the last few tokens.
Is user agent parsing reliable?
No, and it cannot be. The User-Agent header is a free-text field the client chooses; there
is no grammar that identifies a browser unambiguously, no registry of values, and nothing preventing any
client from sending any string. Every parser, including this one, is a pile of ordered heuristics that
must be updated as vendors change their formats. Treat a parse as a guess that is usually right, never as
a fact you can depend on.
Why are modern user agent strings frozen or reduced?
Because the string was a fingerprinting vector. A detailed UA carrying a precise browser build, an
exact OS point release and a device model contributes a lot of entropy to identifying an individual
browser across sites. Chrome, Edge and Safari now report reduced strings: minor version components are
frozen at zero, and OS versions and device models are generalised or fixed. That is why a parser may
report a version like 120.0.0.0, or an iPhone reporting an OS version that has not changed in
years.
What are User-Agent Client Hints?
They are the sanctioned replacement for parsing the header. Instead of one opaque string, the browser
exposes structured values — brand and version list, platform, mobile flag — through the
Sec-CH-UA request headers and the navigator.userAgentData JavaScript API.
Low-entropy hints are sent by default; high-entropy ones such as the full platform version or the device
model must be requested explicitly, which makes the privacy cost visible. Support is currently
Chromium-only, so a UA fallback is still needed.
Should I use UA sniffing in my application?
Almost never for deciding what code to run. Use feature detection: test for the capability you need rather than guessing from the browser name, because that stays correct when a browser gains the feature and when an unknown browser already has it. UA parsing is legitimate for analytics aggregates, for reproducing a reported bug, for routing app-store download links, and for identifying bots and API clients in server logs.
Is the user agent I paste sent to your servers?
No. Parsing happens entirely in this page with JavaScript, so nothing you paste is uploaded, logged or stored. That matters more than it sounds: a UA string pulled from a production access log can be surprisingly identifying on its own, since it may carry an app build number, a device model or an internal client name.
Related tools and reference
API Tester
Send a request with a custom User-Agent and see how the server responds.
cURL Converter
See which UA a copied curl command is carrying, in any language.
CORS Tester
Why a browser will not let a page set its own User-Agent.
HTTP Headers
Where User-Agent sits among the request headers that matter.
Regex Tester
Build and check the patterns a log-parsing pipeline uses on these strings.
403 Forbidden
The status a missing User-Agent most often produces.