All posts

How to read a user-agent string

What each part of a user-agent string means, why they all start with Mozilla/5.0, Client Hints, and how to tell real Googlebot and AI crawlers from fakes.

By Last reviewed 7 min readHTTPUser agentBotsrobots.txt

Every HTTP request from a browser carries a User-Agent header, and most server logs record it. Read naively, it lies: Chrome says it is Safari, Safari says it is like Gecko, and Windows 11 says it is Windows 10. Read part by part, it tells you a lot, as long as you remember that the client could have written anything.

Paste any string from this guide into the User-Agent Parser to see each part explained. It also shows your own browser's string and Client Hints, and counts the bots in a pasted access log.

The format: products and comments

RFC 9110, the HTTP semantics standard, defines the header in section 10.1.5 as one or more product identifiers, each optionally followed by comments:

User-Agent = product *( RWS ( product / comment ) )
product    = token ["/" product-version]

A product is a name and an optional version, such as Chrome/151.0.0.0. A comment is anything in parentheses, such as (Windows NT 10.0; Win64; x64), and by convention its parts are separated by semicolons. The RFC says products are listed "in decreasing order of their significance", and that a user agent "SHOULD NOT generate a User-Agent header field containing needlessly fine-grained detail", because long strings help fingerprint users. Browsers ignore the first: their most telling token comes last. Chrome's UA reduction, below, is the second in practice.

Why every user agent starts with Mozilla/5.0

Netscape Navigator, whose code name was Mozilla, put that name first in its user agent. Sites began sending their best pages only to clients that said "Mozilla", so Internet Explorer said it too and put its real name in a comment, as in Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0). Later browsers did the same. Apple's WebKit, a fork of the KHTML engine, added (KHTML, like Gecko) so Safari would get the pages written for Gecko browsers such as Firefox, and Chrome, which started on WebKit, copied Safari's tokens. MDN puts it plainly: Mozilla/5.0 is sent "for historical reasons" by almost every browser. Skip it; it means nothing.

Three real strings, left to right

Chrome on Windows

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
PartWhat it says
(Windows NT 10.0; Win64; x64)Windows 10 or 11 on a 64-bit x86 CPU. Chrome sends this exact text on every Windows PC.
AppleWebKit/537.36Frozen. Chrome's Blink engine split from WebKit in 2013 but kept the token.
(KHTML, like Gecko)Compatibility text inherited from Safari.
Chrome/151.0.0.0The browser: Chrome 151. The 0.0.0 is deliberate.
Safari/537.36Compatibility again. Real Safari sends Version/ and no Chrome/.

Edge, Opera, Samsung Internet and Yandex add their own token, such as Edg/151.0.0.0 or OPR/124.0.0.0. Brave and Vivaldi send Chrome's string unchanged, so a server can't tell them from Chrome by this header.

Safari on an iPhone

Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1

The comment says iPhone and iOS 18.6, but this is Safari 26, which only runs on iOS 26. Safari 26 froze the OS version in its user agent, and the WebKit team has said it plans to keep it constant, so Version/26.0 is the field to trust. AppleWebKit/605.1.15 and Mobile/15E148 are frozen too. On an iPad, Safari sends the Mac string, Macintosh; Intel Mac OS X 10_15_7, unless it is an iPad mini; the same Intel text appears on Apple silicon Macs.

Googlebot Smartphone

Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.122 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)

Everything up to Safari/537.36 describes a Chrome browser on an old Nexus 5X phone, because Googlebot renders pages like that browser would. The last comment is the identity: compatible, the bot's product token Googlebot/2.1, and a link to Google's documentation, the pattern most crawlers follow. Google documents the Chrome part as Chrome/W.X.Y.Z, a placeholder for the Chromium version its renderer currently uses.

UA reduction and Client Hints

Chrome has been shrinking its user agent since 2022. Chrome 101 set the minor, build and patch numbers to 0.0.0; Chrome 107 on desktop and Chrome 110 on Android froze the platform as well, so every Android device reports Linux; Android 10; K and every Mac Macintosh; Intel Mac OS X 10_15_7. Firefox caps the reported versions at Windows NT 10.0 and macOS 10.15.

The detail moved to User-Agent Client Hints, which Chromium browsers send only over HTTPS. Three low-entropy hints arrive with every request:

Sec-CH-UA: "Chromium";v="151", "Google Chrome";v="151", "Not_A Brand";v="24"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Platform: "Windows"

The "Not_A Brand" entry is GREASE, a fake brand whose spelling changes so that servers stop matching the list exactly. For more, the server asks in a response header, and the browser includes those hints on later requests:

Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Full-Version-List

Sec-CH-UA-Platform-Version: "19.0.0"
Sec-CH-UA-Full-Version-List: "Chromium";v="151.0.7801.62", "Google Chrome";v="151.0.7801.62", "Not_A Brand";v="24.0.0.0"

Microsoft's rule for Windows: a platform version of 13 or higher is Windows 11, 1 to 10 is Windows 10, and 0 is an older release. Pages can read the same values with navigator.userAgentData.getHighEntropyValues(). Firefox and Safari don't support Client Hints, so for them the string is all you get.

Telling real bots from fakes

Scrapers often send Googlebot's string to get past rules that block everyone else. The string proves nothing; the IP address can. Google's documented check is a reverse lookup, then a forward lookup:

$ host 66.249.66.1
1.66.249.66.in-addr.arpa domain name pointer crawl-66-249-66-1.googlebot.com.
$ host crawl-66-249-66-1.googlebot.com
crawl-66-249-66-1.googlebot.com has address 66.249.66.1

The name must end in googlebot.com (google.com or googleusercontent.com for Google's special crawlers and user-triggered fetchers), and the forward lookup must return the original IP; checking only the name is not enough, because whoever controls an IP can make its reverse record say anything. Bing documents the same test with search.msn.com, and Apple with applebot.apple.com.

The other method is a published IP list. Google publishes JSON files such as common-crawlers.json, OpenAI publishes gptbot.json, searchbot.json and chatgpt-user.json, and Anthropic publishes one list for all its bots. Each file holds CIDR ranges; the IP Subnet Calculator shows whether an address falls inside one, and CIDR and subnet masks explained covers the notation. Some newer agents also sign their requests with HTTP Message Signatures (RFC 9421), a scheme called Web Bot Auth.

AI crawlers, AI fetchers and robots.txt

AI companies run three kinds of agents, and robots.txt treats them differently:

  • Training crawlers collect pages that may train models: GPTBot (OpenAI), ClaudeBot (Anthropic), CCBot (Common Crawl) and meta-externalagent (Meta). They follow robots.txt groups that name them. Google-Extended and Applebot-Extended are tokens only: no request carries them. They decide whether pages fetched by Google's and Apple's usual crawlers may train their models, and for Google, ground Gemini's answers.
  • AI search crawlers index pages for answers that link back: OAI-SearchBot, Claude-SearchBot and PerplexityBot. Blocking them can keep your pages out of those answers.
  • User-triggered fetchers load a page because a person asked: ChatGPT-User, Claude-User, Perplexity-User. OpenAI says robots.txt rules "may not apply" to ChatGPT-User, Perplexity says Perplexity-User "generally ignores" them, and Google says the same of its user-triggered fetchers. Anthropic says all three of its bots honour robots.txt.

To opt out of training while staying in search, name the training crawlers and leave the search bots alone:

User-agent: GPTBot
User-agent: ClaudeBot
User-agent: CCBot
User-agent: Google-Extended
Disallow: /

The robots.txt Generator & Tester has these tokens as checkboxes, each linked to its operator's page, and tests a URL against the result. Robots.txt (RFC 9309) is a request, not access control: a client that ignores it, or sends a browser's user agent, needs blocking at the server.

Detect features, not browsers

Code that branches on the user agent breaks in quiet ways. A check for "Windows NT 10.0" can't see Windows 11, a check for "Chrome" also matches Edge, Brave and Opera, a check for "iPad" misses every iPad that sends the Mac string, and a check of the iOS version reads 18.6 on iOS 26. Ask the browser what it can do instead:

if ("share" in navigator) showShareButton();
if (matchMedia("(pointer: coarse)").matches) useLargerTapTargets();

In CSS, @supports and @media (pointer: coarse) do the same. Keep user-agent parsing for what it is good at: reading logs, counting crawlers and debugging a report from one user. When you do, read the string part by part, check the IP before believing a bot, and paste anything strange into the User-Agent Parser.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.