A high-performance, production-grade HTTP User-Agent parser for Go with first-class bot and AI crawler detection.
- 585 detection rules: 124 browsers, 313 bots, 76 devices, 59 in-app browsers, 13 engines
- 35 OS types: Windows (incl. Windows 11 via Client Hints), macOS, iOS, iPadOS, Android, ChromeOS, Linux distros (Ubuntu, Fedora, Debian, Arch, etc.), HarmonyOS, Tizen, KaiOS, FreeBSD, watchOS, tvOS, and more
- First-class bot detection: 313 bots across 7 classes — search engines, social bots, AI crawlers (GPTBot, ClaudeBot, DeepSeekBot, Bytespider), SEO tools, monitoring, scrapers, security scanners
- 14 heuristic patterns: Catches unnamed bots via keywords (bot, crawler, spider, scraper, fetcher, archiver, validator, monitoring, etc.)
- Client Hints support:
Sec-CH-UA-*headers for accurate modern browser detection, Windows 11 detection - Browser channel detection: nightly, beta, dev, canary from version patterns
- High performance: ~500K+ parses/sec per core, zero-alloc cache hits at 32ns
- Concurrency-safe: Immutable Parser, sharded LRU cache (16 shards, 82ns contended)
- Framework-ready: Middleware examples for Gin, Echo, Chi, Fiber, Prometheus +
ParseRequest(*http.Request) - Observability: OpenTelemetry attribute helper (
contrib/otel), Prometheus example, pre/post-parse hooks - Extensible: Custom rules, pre/post-parse hooks
- Pure Go: Zero external runtime dependencies
package main
import (
"fmt"
uax "github.com/motiv8-team/go-ua-parser"
)
func main() {
r := uax.Parse("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/123.0.6312.86 Safari/537.36")
fmt.Println(r.Browser.Name) // "Chrome"
fmt.Println(r.Browser.Version) // "123.0.6312.86"
fmt.Println(r.Browser.Major) // "123"
fmt.Println(r.Browser.Family) // "Chromium"
fmt.Println(r.Engine.Name) // "Blink"
fmt.Println(r.OS.Name) // "Windows"
fmt.Println(r.CPU.Architecture)// "x86_64"
fmt.Println(r.DeviceClass()) // "desktop"
fmt.Println(r.IsBot) // false
fmt.Println(r.IsMobile) // false
}go get github.com/motiv8-team/go-ua-parserRequires Go 1.22+. Zero external dependencies.
// Global one-liner (uses sync.Once default parser)
r := uax.Parse(ua)
// Reusable parser (recommended for servers)
parser, err := uax.NewParser()
r := parser.ParseString(ua)
// With Client Hints
r := parser.Parse(uax.Input{UAString: ua, ClientHints: ch})
// From *http.Request (auto-extracts UA + all Client Hints headers)
r := parser.ParseRequest(httpReq)
// Zero-alloc variants (reuse caller-owned Result)
parser.ParseInto(input, &result)
parser.ParseRequestInto(httpReq, &result)
// Bot-only fast path
bot := parser.DetectBot(ua)type Result struct {
Browser Browser // Name, Version, Major, Family, Channel
Engine Engine // Name, Version
OS OS // Name, Version, Major, Minor, Patch
CPU CPU // Architecture, Bits
Device Device // Type, Vendor, Model, IsPhone, IsTablet, IsDesktop, IsTV, IsTouch
App App // Name, Version, Kind (in-app browsers)
Bot Bot // IsBot, Name, Class, Vendor, Version, IsVerified, Confidence
IsMobile bool // Device is a phone
IsDesktop bool // Device is desktop/laptop
IsTablet bool // Device is a tablet
IsBot bool // Any kind of bot/crawler
IsCrawler bool // Specifically a search/SEO crawler
IsInApp bool // In-app browser (Facebook, Instagram, etc.)
}
// Convenience methods
r.ShortBrowser() // "Chrome 123"
r.ShortOS() // "Windows 10"
r.DeviceClass() // "mobile", "desktop", "tablet", "smarttv", "console", "car", "wearable", "unknown"bot := parser.DetectBot(ua)
if bot.IsBot {
fmt.Println(bot.Name) // "GPTBot"
fmt.Println(bot.Class) // "ai"
fmt.Println(bot.Vendor) // "OpenAI"
fmt.Println(bot.IsVerified) // false
fmt.Println(bot.Confidence) // 1.0
}Bot classes: search | social | ai | seo-tool | monitor | scraper | other
313 named bots including:
| Class | Examples |
|---|---|
| Search | Googlebot, Bingbot, YandexBot, Baiduspider, DuckDuckBot, Applebot |
| Social | Twitterbot, facebookexternalhit, LinkedInBot, Slackbot, Discordbot, TelegramBot, WhatsApp |
| AI | GPTBot, ChatGPT-User, ClaudeBot, Claude-SearchBot, PerplexityBot, DeepSeekBot, Bytespider, CCBot, Amazonbot, Google-Extended, xAI-Bot |
| SEO | AhrefsBot, SemrushBot, MJ12bot, DotBot, Screaming Frog, SISTRIX, Seobility |
| Monitor | UptimeRobot, Pingdom, Datadog, NewRelic, Site24x7, GTmetrix, Uptime-Kuma, PRTG, Nagios |
| Scraper | curl, wget, python-requests, Scrapy, PhantomJS, HeadlessChrome, Playwright, Puppeteer, Shodan, Nmap |
Plus 14 heuristic keyword patterns that catch unnamed bots.
// Simple LRU cache (32ns cache hit, 0 allocs)
cached := uax.NewCachedParser(parser, 10000)
r := cached.ParseString(ua)
// Sharded cache for high concurrency (82ns contended, 0 allocs)
sharded := uax.NewShardedCache(parser, 16, 1000) // 16 shards x 1000 entries
r := sharded.ParseString(ua)
// Stats
stats := cached.Stats() // or sharded.Stats()
fmt.Println(stats.Hits, stats.Misses, stats.Size)Override or extend builtin detection at parser creation time:
parser, _ := uax.NewParser(
uax.WithCustomBotRules([]uax.BotRule{
{Token: "InternalBot", Name: "Our Bot", Class: uax.BotMonitor, Vendor: "Us", Match: "exact"},
}),
uax.WithCustomBrowserRules([]uax.BrowserRule{
{Token: "MyApp", Name: "My App", Family: "Chromium", Engine: "Blink", Match: "exact"},
}),
uax.WithCustomDeviceRules([]uax.DeviceRule{
{Token: "MyKiosk", Type: "embedded", Vendor: "Acme", Model: "Kiosk v2", Match: "exact"},
}),
)Custom rules are checked before builtins, so they can override default behavior.
parser, _ := uax.NewParser(
uax.WithPreParseHook(func(input uax.Input) {
log.Println("Parsing:", input.UAString[:50])
}),
uax.WithPostParseHook(func(input uax.Input, result uax.Result, d time.Duration) {
metrics.ParseDuration.Observe(d.Seconds())
if result.IsBot {
metrics.BotRequests.Inc()
}
}),
)Hooks are nil-checked — zero cost when not configured.
Modern Chromium browsers send reduced UA strings. Client Hints provide accurate detection:
// Manual
r := parser.Parse(uax.Input{
UAString: req.Header.Get("User-Agent"),
ClientHints: uax.ClientHintsFromMap(map[string]string{
"Sec-CH-UA": req.Header.Get("Sec-CH-UA"),
"Sec-CH-UA-Platform": req.Header.Get("Sec-CH-UA-Platform"),
"Sec-CH-UA-Platform-Version": req.Header.Get("Sec-CH-UA-Platform-Version"),
"Sec-CH-UA-Arch": req.Header.Get("Sec-CH-UA-Arch"),
"Sec-CH-UA-Model": req.Header.Get("Sec-CH-UA-Model"),
"Sec-CH-UA-Full-Version": req.Header.Get("Sec-CH-UA-Full-Version"),
}),
})
// Automatic (recommended)
r := parser.ParseRequest(req)Client Hints fields override UA-derived values. Windows 11 is detected when Sec-CH-UA-Platform-Version >= 13.
r := parser.ParseString("Mozilla/5.0 ... Firefox/126.0a1")
r.Browser.Channel // "nightly"
r := parser.ParseString("Mozilla/5.0 ... Firefox/125.0b9")
r.Browser.Channel // "beta"Detected channels: nightly, beta, dev, canary (from version patterns and Client Hints brands).
parser, _ := uax.NewParser(
uax.WithBotDetection(false), // disable bot detection
uax.WithCustomBrowserRules([]uax.BrowserRule{}), // custom browser rules
uax.WithCustomBotRules([]uax.BotRule{}), // custom bot rules
uax.WithCustomDeviceRules([]uax.DeviceRule{}), // custom device rules
uax.WithPreParseHook(fn), // pre-parse callback
uax.WithPostParseHook(fn), // post-parse callback with timing
)Each framework example is a separate Go module (no framework deps in the main library).
func UAMiddleware(parser *uax.Parser) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result := parser.ParseRequest(r)
ctx := context.WithValue(r.Context(), uaKey, result)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}func UAMiddleware(parser *uax.Parser) gin.HandlerFunc {
return func(c *gin.Context) {
result := parser.ParseString(c.GetHeader("User-Agent"))
c.Set("ua_result", result)
c.Next()
}
}func UAMiddleware(parser *uax.Parser) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
result := parser.ParseString(c.Request().Header.Get("User-Agent"))
c.Set("ua_result", result)
return next(c)
}
}
}func UAMiddleware(parser *uax.Parser) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
result := parser.ParseRequest(r)
ctx := context.WithValue(r.Context(), ctxKey{}, result)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}func UAMiddleware(parser *uax.Parser) fiber.Handler {
return func(c *fiber.Ctx) error {
result := parser.ParseString(c.Get("User-Agent"))
c.Locals("ua_result", result)
return c.Next()
}
}Complete working examples in examples/gin/, examples/echo/, examples/chi/, examples/fiber/.
Benchmarked on Apple M3 Pro (arm64):
Parse performance:
| User-Agent Type | ns/op | B/op | allocs/op |
|---|---|---|---|
| curl (short bot) | 284 | 0 | 0 |
| Googlebot | 1,121 | 112 | 2 |
| GPTBot (AI) | 1,325 | 144 | 2 |
| Chrome Desktop | 1,852 | 272 | 5 |
| Edge Desktop | 1,947 | 288 | 5 |
| Firefox Desktop | 2,027 | 160 | 3 |
| Android Chrome | 2,150 | 272 | 5 |
| Safari Mobile | 2,529 | 336 | 7 |
| Samsung TV | 2,598 | 264 | 5 |
| Facebook In-App | 3,553 | 336 | 7 |
Cache performance:
| Cache Type | ns/op | allocs/op |
|---|---|---|
| LRU cache hit | 32 | 0 |
| Sharded cache hit | 64 | 0 |
| Sharded contended (parallel) | 82 | 0 |
Internal components:
| Component | ns/op | allocs/op |
|---|---|---|
| Tokenizer | 75 | 0 |
| Trie lookup | 4 | 0 |
Run benchmarks:
go test -bench Benchmark -benchmem -count=3Three-stage parsing pipeline:
UA String ──→ [Tokenizer] ──→ [Matcher] ──→ [Assembler] ──→ Result
│ │ │
zero-alloc trie (4ns) Client Hints
fixed buffer + regex merge + derive
- Tokenize (
tokenizer.go): Zero-alloc scanner extracts product tokens (name/version) and comment blocks using a fixed[24]tokenbuffer. - Match (
match_*.go,matcher.go,trie.go): Hybrid trie + linear-scan matcher. Trie handles exact-match rules in O(key-length); contains/prefix rules fall back to indexed scan. - Assemble (
merge.go): Merges Client Hints (CH takes precedence), computes convenience booleans.
Rules are defined in rules/*.yaml and compiled to Go source via cmd/uagen. Generated rules_gen_*.go files are committed — consumers never run the generator.
| Category | Count | Description |
|---|---|---|
| Browsers | 124 | Chrome, Firefox, Safari, Edge, Opera, Brave, Vivaldi, Samsung Internet, Arc, Whale, DuckDuckGo, 100+ more |
| Engines | 13 | Blink, WebKit, Gecko, Trident, Presto, EdgeHTML, Goanna, KHTML, Servo, NetSurf, and more |
| Bots | 313 | Search, social, AI, SEO, monitor, scraper, security scanners, HTTP libraries |
| Devices | 76 | iPhones, iPads, Samsung Galaxy, Pixel, Xiaomi, consoles (PS5, Xbox, Switch), TVs, Kindle, Tesla, Apple Watch |
| In-App | 59 | Facebook, Instagram, TikTok, WeChat, Telegram, Slack, Teams, Spotify, and 50+ more |
| OS | 35 | Windows (10/11), macOS, iOS, iPadOS, Android, ChromeOS, 15+ Linux distros, HarmonyOS, Tizen, KaiOS, BSD variants |
| Heuristics | 14 | bot, crawler, spider, scraper, fetcher, archiver, validator, monitoring, analyzer, and more |
Rules are maintained in YAML and compiled to Go:
cd cmd/uagen && go run .This reads rules/*.yaml and generates rules_gen_*.go in the repo root. Generated files are committed — library consumers never run the generator or need gopkg.in/yaml.v3.
To add a new rule, edit the appropriate YAML file and regenerate:
# rules/bots.yaml
- token: "MyNewBot"
name: "My New Bot"
class: monitor
vendor: "My Company"
match: exactSee CONTRIBUTING.md.
The contrib/otel module provides span attribute helpers:
import uaxotel "github.com/motiv8-team/go-ua-parser/contrib/otel"
attrs := uaxotel.Attributes(result) // []attribute.KeyValue
span.SetAttributes(attrs...)Only non-empty fields are emitted, using the http.user_agent.* attribute namespace.