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
SegmentKindMeaning
Mozilla/5.0ProductVestigial compatibility prefix. Meaningless.
(Linux; Android 14; SM-S928B)CommentKernel, OS version, device model
AppleWebKit/537.36ProductEngine lineage. 537.36 is frozen and tells you nothing.
(KHTML, like Gecko)CommentCompatibility claim from WebKit's KDE origins
Chrome/131.0.0.0ProductThe real browser and version
MobileBare tokenForm factor. Its absence on Android means tablet.
Safari/537.36ProductMore compatibility padding

Where the real version lives

This is where most parsers go wrong. The meaningful version is rarely the first one.

BrowserRead this tokenNot this
ChromeChrome/131.0.0.0AppleWebKit/537.36 (frozen)
SafariVersion/18.2Safari/604.1 (a WebKit build number)
EdgeEdg/131.0.0.0Chrome/131.0.0.0 (the underlying Chromium)
OperaOPR/115.0.0.0Chrome/130.0.0.0
Samsung InternetSamsungBrowser/27.0Chrome/125.0.0.0
Opera (Presto)Version/12.18 at the endOpera/9.80 at the start (frozen)
IE 11rv:11.0No MSIE token exists
The ordering rule. Chromium forks all keep the 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

PlatformComment 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, alongside Version/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.