If you want to know whether your visitor is on mobile or desktop, the obvious answer is “parse the User-Agent string.” That works most of the time, but it’s surprisingly fragile, increasingly opt-in, and gives you less than you might expect. The IP layer — specifically the ASN the user is connecting from — adds an important secondary signal when User-Agent isn’t enough.
This post walks through the practical approaches: User-Agent parsing, modern client hints, IP/ASN-based heuristics, and the combined patterns that work in production.
Why You Want to Know
A few use cases for “is this mobile?”:
- Responsive design fallbacks. CSS handles most of this, but server-side rendering may need to choose templates per device.
- Analytics segmentation. Distinguishing mobile and desktop traffic is fundamental for product analytics.
- Mobile-specific features. Push notifications, app-store deeplinks, mobile-only download CTAs.
- Conversion-rate optimization. Mobile and desktop convert differently — measuring each separately matters.
- Performance budgets. Different image sizes, different feature levels for mobile clients.
The accuracy bar isn’t “100%.” It’s “right often enough to be useful for the specific decision you’re making.”
Method 1: User-Agent String Parsing
The traditional approach. Every browser sends a User-Agent header that includes device hints:
Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
The patterns:
- Mobile: Usually includes
Mobile,Android,iPhone,iPad,iPod. - Desktop: Includes
Windows,Macintosh,Linux x86_64. - Tablet:
iPadorAndroidwithoutMobile(yes, Android tablets specifically omitMobile).
A naive parse:
function isMobile(ua) {
return /Mobile|Android|iPhone|iPod/i.test(ua)
}
function isTablet(ua) {
return /iPad|Tablet|Android(?!.*Mobile)/i.test(ua)
}
Works for ~95% of traffic. For production, use a library like ua-parser-js (Node), user-agents (Python), or your framework’s built-in (Laravel has Agent).
Pros:
- Available on every request (well, every request that sends a UA).
- Server-side, no client cooperation needed.
- Mature libraries that handle the edge cases.
Cons:
- Slow erosion. Browsers (Chrome especially) are reducing UA detail over time. Future UAs will be less specific.
- Easy to spoof. Headless browsers, scrapers, and corporate-managed browsers can send anything.
- Doesn’t distinguish “phone-sized tablet” from “phone” or “small laptop” from “tablet.”
- Inconsistent with new device types. Foldables, gaming handhelds, anything new — coverage in libraries lags.
Method 2: Client Hints
The modern replacement for User-Agent. Servers request specific hints via Accept-CH; browsers respond with Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA, etc.
Response: Accept-CH: Sec-CH-UA-Mobile, Sec-CH-UA-Platform
Subsequent request:
Sec-CH-UA-Mobile: ?1 (1 = mobile, 0 = not)
Sec-CH-UA-Platform: "Android"
Sec-CH-UA: "Google Chrome";v="120"
In your backend:
const isMobile = req.headers['sec-ch-ua-mobile'] === '?1'
const platform = req.headers['sec-ch-ua-platform']?.replace(/"/g, '')
Pros:
- More accurate than UA parsing for the specific properties exposed.
- Future-proof — UA is being deprecated; client hints are the replacement.
- Easier to parse (structured headers, not regex).
Cons:
- Requires a round trip to request the hints first (most browsers don’t send them by default).
- Not all browsers support them yet. Safari support is partial.
- Easier for privacy tools to strip than UA.
For production in 2026, use client hints when they’re sent, fall back to UA parsing otherwise.
Method 3: IP / ASN-Based Heuristics
Where the IP layer adds value. The ASN — and the type of ASN — gives signals about the user’s network:
- Mobile carrier ASN (T-Mobile, Verizon Wireless, Vodafone, Reliance Jio, etc.) → almost certainly mobile data, almost certainly a mobile device.
- Residential ISP ASN (Comcast, BT, Deutsche Telekom) → could be either; favor UA signal.
- Business / fiber ASN → desktop heavily favored.
- Cloud / hosting ASN (AWS, GCP, Azure, Hetzner) → bot, automation, or developer.
- VPN ASN → user could be anything; signal is unreliable.
You can look up an IP’s ASN cheaply and classify it:
const result = await convertIP(ip)
const asnType = classifyAsn(result.data.asn.number, result.data.asn.name)
if (asnType === 'mobile') {
// Very likely mobile device on cellular data
}
if (asnType === 'hosting') {
// Almost certainly not a real user
}
The classification logic itself is a curated list — major mobile carriers, major hosting providers, major VPN services. Services like Ip2Geo’s API include this classification inline; you don’t have to build it.
Pros:
- Cannot be spoofed by the user (you read the IP from your network layer, not from a header).
- Useful when User-Agent is missing or stripped.
- Catches obvious automation (cloud IPs) that look like normal browsers in UA.
Cons:
- Coarse-grained. Tells you “user is on a phone network” but not “phone vs tablet on that network.”
- Mobile vs non-mobile is fuzzy on shared infrastructure like enterprise Wi-Fi which routes via the corporate network’s ISP.
- CGNAT obscures details (see the NAT post).
The Combined Strategy
A robust detection looks at all three signals:
async function detectDevice(req: Request, ip: string) {
// 1. Modern client hints (best signal when present)
const mobileHint = req.headers['sec-ch-ua-mobile']
if (mobileHint === '?1') return 'mobile'
if (mobileHint === '?0') return 'desktop'
// 2. User-Agent parsing (default for most current traffic)
const ua = req.headers['user-agent'] ?? ''
if (/iPad|Android(?!.*Mobile)|Tablet/i.test(ua)) return 'tablet'
if (/Mobile|Android|iPhone|iPod/i.test(ua)) return 'mobile'
// 3. IP/ASN fallback (when UA is missing or suspicious)
const result = await convertIP(ip)
if (result.success) {
const asnType = classifyAsn(result.data.asn.number)
if (asnType === 'mobile') return 'mobile'
if (asnType === 'hosting') return 'bot'
}
// 4. Default
return 'desktop'
}
For most apps, the order matters more than the details. Use the strongest available signal first.
Common Mistakes
Trusting User-Agent blindly
A scraper sending Mozilla/5.0 (iPhone...) will be classified as mobile. If your decision changes business outcomes (showing different pricing, etc.), this is exploitable.
Treating tablets as desktop
Default iPad and Android tablets often have a desktop-class UA but a touch interface. Categorize them explicitly.
Server-side decisions that should be client-side
Responsive design happens in CSS. Don’t render a completely different HTML page for mobile when CSS media queries can handle it. Server-side mobile/desktop detection should be reserved for things CSS can’t do (template choice, feature flags).
Caching the wrong way
If you cache a page server-side rendered for mobile, don’t serve it to desktop visitors. Vary the cache by User-Agent device class or skip caching for variable content.
Forgetting bot traffic
Scrapers, search engines, monitoring tools — none of these are “mobile” or “desktop” in a meaningful sense. Detect bots first via UA pattern matching (bot, crawler, spider) and ASN (hosting providers are usually bots) before classifying the rest.
What Each Signal Tells You
A quick reference:
| Signal | Tells you… | Reliable when… | Spoofable? |
|---|---|---|---|
| UA string | Browser, OS, device hints | UA is detailed and present | Yes |
| Client hints | Structured device info | Browser supports them | Less easily |
| ASN classification | Network type | You have a curated list | No |
| IP geolocation | Country, region | Always | No (without VPN) |
| Screen size (JS) | Actual viewport | Client cooperates | Yes |
| Touch events (JS) | Touch capability | Client cooperates | Yes |
The strongest single signal is server-side ASN classification — you can’t spoof your network of origin. The most precise signal is client-side fingerprinting (touch events, screen size, hover capability) — which requires JavaScript to run.
For SEO and bot detection, combine all of the above.
A Concrete Implementation
Putting it together for a real app:
import { Request } from 'express'
import { convertIP, init } from '@ip2geo/sdk'
init({ authKey: process.env.IP2GEO_API_KEY! })
const HOSTING_ASNS = new Set([16509, 14061, 15169, 8075, ...]) // AWS, DO, Google, Microsoft, etc.
const MOBILE_ASNS = new Set([20057, 22394, 21928, ...]) // Major mobile carriers
export type DeviceClass = 'mobile' | 'tablet' | 'desktop' | 'bot' | 'unknown'
export async function classifyDevice(req: Request): Promise<DeviceClass> {
const ua = (req.headers['user-agent'] ?? '').toLowerCase()
// 1. Quick bot detection
if (/bot|crawler|spider|httpclient|wget|curl/i.test(ua)) return 'bot'
// 2. ASN check for non-residential IPs
try {
const result = await convertIP(req.ip)
if (result.success && result.data?.asn?.number) {
const asn = result.data.asn.number
if (HOSTING_ASNS.has(asn)) return 'bot'
}
} catch {} // continue if API fails
// 3. Tablet detection (specific Android tablets and iPad)
if (/ipad/.test(ua) || /android(?!.*mobile)/.test(ua)) return 'tablet'
// 4. Mobile via UA
if (/mobile|android|iphone|ipod/.test(ua)) return 'mobile'
// 5. Modern client hints
const mobileHint = req.headers['sec-ch-ua-mobile']
if (mobileHint === '?1') return 'mobile'
// 6. Default to desktop
return 'desktop'
}
This catches the major cases, fails open (returns ‘desktop’ rather than crashing), and uses ASN data as backup when UA is missing or stripped.
TL;DR
- User-Agent parsing is the default but is being deprecated by browsers.
- Client hints (
Sec-CH-UA-Mobile) are the replacement — use them when available. - IP/ASN classification adds a fraud-resistant signal when UA can’t be trusted.
- Combine all three for the most robust detection.
- Don’t make destructive decisions on this signal — it’s a heuristic, not a verdict.
- Cache lookups if you’re checking ASN per-request.
- Detect bots first, then classify the rest.
For more on what the IP layer can tell you, see How to Geolocate an IP and What is an ASN. To try ASN-based classification on real traffic, the Ip2Geo API returns the classified ASN type inline with every geo lookup.