User Agent Parsing Guide: UA Structure, Client Hints, and Bot Detection
Every HTTP request carries a User-Agent header — a string that is supposed to identify the client making the request. In practice, it is one of the most misunderstood and misused pieces of data in web development. Parse it with the User Agent Parser to follow along with the examples below.
Anatomy of a Typical User Agent String
Take this UA from Chrome 122 on Windows:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36Breaking it down token by token:
| Token | Meaning |
|---|---|
Mozilla/5.0 | Legacy compatibility prefix — present in virtually every modern browser |
(Windows NT 10.0; Win64; x64) | OS and architecture — Windows 10, 64-bit |
AppleWebKit/537.36 | Rendering engine — Blink (Chrome's fork of WebKit) reports this |
(KHTML, like Gecko) | Another compatibility token; KHTML was an early open-source layout engine |
Chrome/122.0.0.0 | Actual browser and major version |
Safari/537.36 | Trailing Safari token — present in all Chromium-based browsers |
Notice that the string mentions Mozilla, WebKit, KHTML, Gecko, and Safari — yet the browser is Chrome. This is not a bug. It is the product of twenty-five years of incremental compatibility theater.
Historical Baggage: Why Every Browser Lies
The UA string mess started in the mid-1990s. Netscape Navigator called itself Mozilla/1.0. When Internet Explorer launched, some servers only delivered rich content to Mozilla/* agents, so IE identified itself as Mozilla/2.0 (compatible; MSIE 3.0) to get through the gate. Then sites started gating on Gecko, so browsers added like Gecko tokens. Then on AppleWebKit, so Chromium added that. Then on Safari, so Chromium added that too.
Each round of UA sniffing by web servers drove browsers to impersonate more engines. Today, every Chromium-based browser — Chrome, Edge, Opera, Brave, Arc — sends a UA string that claims to be Mozilla, WebKit, KHTML, Gecko, and Safari simultaneously. Firefox reports Gecko accurately but still leads with Mozilla/5.0.
The practical result: you cannot reliably parse the UA string to determine the real browser engine without a dedicated, regularly updated parser library. And even then, you are parsing a string designed to deceive.
The UA Freezing and Reduction Initiative
Browser vendors recognized that detailed UA strings are a fingerprinting vector — they help third parties track users across sites even without cookies. Starting around 2020, Chrome announced a phased UA reduction plan:
- Minor version numbers are frozen to
0.0.0— you seeChrome/122.0.0.0, notChrome/122.0.6261.112. - The OS version in the UA is reduced to a small set of representative values (e.g., Windows always reports
Windows NT 10.0regardless of the actual build). - On mobile, device model strings are being phased out in favor of a generic
Androidtoken. - Firefox and Safari have implemented similar entropy-reduction policies.
The intent is to make all User-Agent strings in a browser family look identical, eliminating their value as a fingerprinting signal — and as a source of reliable version data.
Client Hints: The Modern Alternative
To fill the gap left by UA reduction, the W3C standardized User-Agent Client Hints (UA-CH). Instead of embedding everything in one opaque header, the browser exposes structured data through separate headers — but only when the server explicitly requests them.
The server opts in by returning an Accept-CH response header:
Accept-CH: Sec-CH-UA, Sec-CH-UA-Platform, Sec-CH-UA-Mobile, Sec-CH-UA-Full-Version-ListOn subsequent requests the browser sends the requested hints:
Sec-CH-UA: "Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"
Sec-CH-UA-Platform: "Windows"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Full-Version-List: "Chromium";v="122.0.6261.112", "Google Chrome";v="122.0.6261.112"The key Client Hints headers:
| Header | Data | Notes |
|---|---|---|
Sec-CH-UA | Brand list + major version | Sent by default on HTTPS |
Sec-CH-UA-Mobile | Boolean — is the device mobile? | Sent by default on HTTPS |
Sec-CH-UA-Platform | OS name ("Windows", "macOS", "Android") | Sent by default on HTTPS |
Sec-CH-UA-Full-Version-List | Brand list + full version string | Requires explicit Accept-CH |
Sec-CH-UA-Platform-Version | OS version | Requires explicit Accept-CH |
Sec-CH-UA-Arch | CPU architecture | Requires explicit Accept-CH |
Client Hints only work over HTTPS, only in Chromium-based browsers (Firefox and Safari do not implement them yet), and require the server to request them. They are more structured and privacy-preserving than the classic UA string, but they are not yet universal.
JavaScript: navigator.userAgentData vs navigator.userAgent
The same data is available in JavaScript. The legacy API returns the raw string:
// Legacy — returns the full opaque string
console.log(navigator.userAgent);
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."The modern API returns structured data, but is only available in Chromium browsers:
// Modern — structured, available in Chromium only
const uaData = navigator.userAgentData;
console.log(uaData.brands);
// [{ brand: "Chromium", version: "122" }, { brand: "Google Chrome", version: "122" }, ...]
console.log(uaData.mobile); // false
console.log(uaData.platform); // "Windows"
// High-entropy values require a promise-based call
const high = await uaData.getHighEntropyValues([
"architecture",
"platformVersion",
"fullVersionList",
]);
console.log(high.platformVersion); // "15.0.0"For cross-browser code, always feature-detect before using navigator.userAgentData:
function isMobile() {
if (navigator.userAgentData) {
return navigator.userAgentData.mobile;
}
// Fallback to UA string parsing for Firefox / Safari
return /Mobi|Android/i.test(navigator.userAgent);
}When UA Parsing Is Still Legitimate
Despite its limitations, UA parsing remains valid in specific scenarios:
- Analytics and reporting: Aggregate traffic breakdowns by browser family, OS, and device type are still useful even with reduced fidelity. Exact version numbers are rarely needed for dashboards.
- Bot and crawler detection: Legitimate crawlers identify themselves with recognizable UAs. Combining UA checks with behavioral signals (request rate, missing headers, JavaScript execution) is a practical first-pass filter.
- Feature-detection fallback: When a CSS feature query or JS capability check is not available, a UA-based heuristic can serve as a last resort — but prefer
@supportsand feature detection over UA sniffing for this use case. - Serving different asset bundles: Delivering a legacy JS bundle to old browsers and a modern bundle to current ones. UA-based routing is coarse but fast.
- Debugging and support tooling: Logging the UA alongside error reports helps reproduce issues on specific browser/OS combinations.
Common Bot and Crawler User Agents
| Bot | User-Agent Token | Owner |
|---|---|---|
| Googlebot | Googlebot/2.1 | Google Search |
| Google AdsBot | AdsBot-Google | Google Ads quality check |
| Bingbot | bingbot/2.0 | Microsoft Bing |
| ChatGPT-User | ChatGPT-User | OpenAI (browsing plugin) |
| GPTBot | GPTBot/1.0 | OpenAI (training crawler) |
| Claude-Web | Claude-Web/1.0 | Anthropic (browsing) |
| ClaudeBot | ClaudeBot/0.5 | Anthropic (training crawler) |
| AhrefsBot | AhrefsBot/7.0 | Ahrefs SEO |
| SemrushBot | SemrushBot/7~bl | Semrush SEO |
| DotBot | DotBot/1.2 | Moz |
| facebookexternalhit | facebookexternalhit/1.1 | Facebook link preview |
| Twitterbot | Twitterbot/1.0 | Twitter/X card preview |
Legitimate crawlers from Google and Bing can be verified by reverse DNS lookup — check that the IP address of the requester resolves to a hostname in googlebot.com or search.msn.com and that the forward lookup matches. UA strings alone can be trivially spoofed.
Parsing Libraries
If you need to parse UA strings programmatically, use a maintained library rather than hand-rolling regex patterns.
# JavaScript
npm install ua-parser-jsimport { UAParser } from 'ua-parser-js';
const parser = new UAParser(request.headers['user-agent']);
const result = parser.getResult();
console.log(result.browser.name); // "Chrome"
console.log(result.browser.version); // "122.0.0.0"
console.log(result.os.name); // "Windows"
console.log(result.device.type); // undefined (desktop) | "mobile" | "tablet"# Python
pip install user-agentsfrom user_agents import parse
ua_string = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..."
ua = parse(ua_string)
print(ua.browser.family) # "Chrome"
print(ua.os.family) # "Windows"
print(ua.is_mobile) # False
print(ua.is_bot) # False# Go
go get github.com/mssola/useragentimport "github.com/mssola/useragent"
ua := useragent.New("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...")
name, version := ua.Browser()
fmt.Println(name) // "Chrome"
fmt.Println(version) // "122.0.0.0"
fmt.Println(ua.OS()) // "Windows"
fmt.Println(ua.Mobile()) // falseAll three libraries share the same trade-off: they rely on maintained signature databases that must be updated as browsers release new versions. Pin your dependency and update regularly, or you will start misidentifying new browser releases as unknown.
Pitfalls and What Not to Do
- Do not use UA for access control. Any client can send any UA string. Using it to gate premium features or security-sensitive routes is trivially bypassed with
curl -H "User-Agent: ...". - Do not assume UA equals identity. Browser extensions frequently modify the UA string. Corporate proxies sometimes rewrite it. Testing tools like Playwright and Puppeteer default to a headless UA but can be configured to send anything.
- Headless browser detection is an arms race. Checking for
HeadlessChromein the UA used to catch automation tools. Modern headless Chrome no longer includes that token. Behavioral signals — mouse movement entropy, timing patterns, WebGL fingerprints — are more reliable than UA checks for anti-bot work. - Do not parse UA to infer CSS feature support. Use
@supports,CSS.supports(), and progressive enhancement instead. A UA-based feature matrix goes stale immediately and is wrong for Edge cases (pun intended). - UA reduction will make version strings less useful over time. Build server-side logic against Client Hints for Chromium users and accept reduced fidelity for Firefox and Safari.
Inspect and parse any user agent string in your browser with the User Agent Parser. For more testing tools, see the Testers Guide.