User agent string format
The grammar behind the noise — what a token is, where versions live, and how to read any string you encounter.
The formal grammar
RFC 9110 defines the header as a sequence of product identifiers, each optionally followed by a comment:
User-Agent = product *( RWS ( product / comment ) )
product = token [ "/" product-version ]
comment = "(" *( ctext / quoted-pair / comment ) ")"
In practice that means: space-separated Name/Version pairs, with parenthesised blocks of semicolon-separated details wherever more context is needed. Everything else is convention, not specification.
Reading a string left to right
Mozilla/5.0 (Linux; Android 14; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36
| Segment | Kind | Meaning |
|---|---|---|
Mozilla/5.0 | Product | Vestigial compatibility prefix. Meaningless. |
(Linux; Android 14; SM-S928B) | Comment | Kernel, OS version, device model |
AppleWebKit/537.36 | Product | Engine lineage. 537.36 is frozen and tells you nothing. |
(KHTML, like Gecko) | Comment | Compatibility claim from WebKit's KDE origins |
Chrome/131.0.0.0 | Product | The real browser and version |
Mobile | Bare token | Form factor. Its absence on Android means tablet. |
Safari/537.36 | Product | More compatibility padding |
Where the real version lives
This is where most parsers go wrong. The meaningful version is rarely the first one.
| Browser | Read this token | Not this |
|---|---|---|
| Chrome | Chrome/131.0.0.0 | AppleWebKit/537.36 (frozen) |
| Safari | Version/18.2 | Safari/604.1 (a WebKit build number) |
| Edge | Edg/131.0.0.0 | Chrome/131.0.0.0 (the underlying Chromium) |
| Opera | OPR/115.0.0.0 | Chrome/130.0.0.0 |
| Samsung Internet | SamsungBrowser/27.0 | Chrome/125.0.0.0 |
| Opera (Presto) | Version/12.18 at the end | Opera/9.80 at the start (frozen) |
| IE 11 | rv:11.0 | No MSIE token exists |
Chrome/ token and append their own. So a parser must check for every fork before checking for Chrome, or every Edge, Opera, Samsung and Vivaldi user is counted as Chrome. The same applies to Safari: check for Chrome and every Chromium fork first, because they all also carry Safari/.
Platform comment conventions
| Platform | Comment block |
|---|---|
| Windows | (Windows NT 10.0; Win64; x64) |
| macOS | (Macintosh; Intel Mac OS X 10_15_7) — underscores, not dots |
| Linux | (X11; Linux x86_64) or with a distro: (X11; Ubuntu; Linux x86_64) |
| Android | (Linux; Android 14; SM-S928B) |
| iOS | (iPhone; CPU iPhone OS 18_2 like Mac OS X) — underscores |
| iPadOS | (iPad; CPU OS 17_5 like Mac OS X) — note CPU OS, not CPU iPhone OS |
| Chrome OS | (X11; CrOS x86_64 15917.71.0) |
Version numbers inside Apple platform comments use underscores rather than dots — 10_15_7, not 10.15.7. A parser that only matches dots misses every Apple platform.
Special tokens worth knowing
Mobile— phone form factor. Its absence on Android implies a tablet.wv— Android WebView. Appears inside the platform comment, alongsideVersion/4.0.like Gecko— a compatibility claim, never an indication of the actual engine.rv:— the real Gecko version, and the only way to identify IE 11.TV,VR— form factor hints on televisions and headsets.compatible;— historically introduced a masquerade; now mostly seen in bot strings.
Frequently asked questions
Is there a maximum length?
No specification limit, but servers impose practical ones — most cap total header size somewhere between 4 KB and 16 KB. Real user agents run from about 10 characters for curl/8.11.1 to roughly 400 for a heavily annotated in-app browser string.
Why do version numbers use underscores on Apple platforms?
Convention inherited from the original Safari implementation, kept for compatibility ever since. A parser handling Apple strings must normalise 10_15_7 to 10.15.7, which is exactly what our parser does.
What order should I check tokens in?
Most specific first. Bots before browsers, then Chromium forks before Chrome, then Chrome before Safari, then Safari before the bare WebKit fallback. Any other order produces silent misclassification rather than an obvious error, which is what makes it such a persistent source of bad analytics.