The best user agent for web scraping is a current desktop Chrome 151 string for Windows. Chrome holds around 69% of the global browser market (StatCounter, June 2026), so it is the least conspicuous choice – pair it with current Firefox, Edge, and mobile strings, plus matching headers like Accept-Language and Sec-CH-UA.
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
A server reads the User-Agent header on every request to decide what content to serve and to flag automated traffic. Python libraries give a scraper away immediately: requests announces itself as python-requests/2.x, and httpx sends its own obvious default, so both often trigger blocks and CAPTCHAs before the first page loads. This guide covers the best user agent for scraping across every major browser and operating system, the matching headers each web scraper needs, how to set and rotate a UA for scraping in Python, and when a managed scraping API is the better option than maintaining strings by hand.
| UA Type | Best For | Notes |
|---|---|---|
| Chrome / Windows | General scraping, SERP scraping, e-commerce | Highest market share, safest default |
| Chrome / macOS | Apple ecosystem targets, SaaS dashboards | Consistent Chromium fingerprint |
| Firefox / Windows | Diversifying rotation pools | Gecko engine adds fingerprint variety |
| Safari / macOS | Apple-centric sites, apple.com | Only real WebKit user agent, can trigger different content |
| Safari / iOS | Mobile API endpoints, AMP pages | Unlocks mobile-only variants and APIs |
| Edge / Windows | B2B and enterprise portals, Microsoft properties | Same Chromium engine as Chrome, smaller share |
| Android Chrome | Mobile pages, lightweight APIs | Different page content served on many sites |
What a User Agent String Actually Is
The User-Agent is a text HTTP header that a browser or client sends with every request, telling the server which browser, rendering engine, and operating system made it. In the browser, JavaScript exposes the same value through navigator.userAgent, while servers receive it in the HTTP User-Agent header.
Modern Chrome no longer tells the whole story in that one header. As part of its User-Agent Reduction work, Chrome now sends a frozen, trimmed user agent string and moves the fine detail into structured Client Hints: Sec-CH-UA lists the browser brand information and major version, Sec-CH-UA-Mobile flags mobile or desktop, and Sec-CH-UA-Platform names the operating system. A site opts into these hints when it wants more than the reduced string offers. Although Chrome now uses the Blink rendering engine internally, its user agent still includes legacy WebKit compatibility tokens for historical compatibility. Here is a current Chrome User-Agent string with each token labeled:
Chrome trimmed the header to reduce passive fingerprinting. The old user agent exposed details such as the exact browser version, operating system build, and device model. Now those details are available through opt-in Client Hints instead. For a scraper, the takeaway is simple: real Chrome sends a predictable string, so anything much more detailed can look suspicious.
The Best User Agents for Web Scraping (2026 List)
Every string below comes from a real browser install and was current at the time of writing. Before deploying, verify that the major versions are still current, because outdated browser versions are a detection signal that firewalls such as Cloudflare, Akamai, and DataDome can flag.
The User-Agent is only one signal in modern bot detection. Firewalls also evaluate TLS fingerprints, request headers, IP reputation, and browser behavior. A realistic User-Agent helps you pass the first layer of checks, but it works best when the rest of the request also matches a real browser.
Chrome User Agents (Windows, macOS, Linux)
Chrome comes first because it has the largest browser market share, making a Chrome User-Agent the safest and least conspicuous default for web scraping. The three strings below cover the desktop platforms you will target most. They differ mainly in the operating system token, while the rest of the User-Agent remains largely the same. Due to Chrome's User-Agent Reduction, minor version numbers are frozen to 0.0.0 and operating system tokens are standardized. As a result, each platform (Windows, macOS, and Linux) has a single canonical User-Agent for a given major release. Real rotation variety comes from mixing in other browser families such as Firefox, Edge, and Safari rather than generating multiple Chrome strings.
Send the matching Client Hints alongside any of these. For Chrome 151 on Windows, they read:
One honest caveat on that block: the last entry ("Not)A;Brand";v="8") is a deliberately varied GREASE value that Chrome changes between releases precisely so servers cannot hardcode it. Treat it as illustrative and copy the exact three-item list from your own browser via navigator.userAgentData in DevTools rather than trusting any fixed example.
Matching Client Hints matter because sophisticated anti-bot checks compare the frozen user agent against Sec-CH-UA. A version mismatch — a current version in the user agent but an old or absent version in the Client Hints — is one of the strongest bot signals a scraper can leak, so the two must always agree.
A current Chrome User-Agent is only half the picture. If the matching Sec-CH-UA headers are missing or report a different version, the request no longer looks like a real browser. Always update the User-Agent and Client Hints together to keep them consistent.
Firefox User Agents
Firefox earns its place in a rotation pool because the Gecko engine is genuinely different from Chromium, not a reskin of it. That difference shows up in the fingerprint a site can measure, from rendering behavior and JavaScript execution to network-level characteristics, so mixing real Firefox traffic into the pool adds variety that a pool of Chromium-only strings cannot fake. A pool that is nominally diverse but entirely Chromium underneath still looks like one browser wearing different names; a genuine Firefox share breaks that pattern.
Firefox does not send Chromium's Sec-CH-UA Client Hints, so do not attach them to a Firefox User-Agent. Keep Accept and Accept-Languageconsistent with what Firefox actually sends instead. For example, a typical Firefox navigation request includes:Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5
Safari User Agents (macOS, iOS)
Safari matters because it is the only widely used real WebKit browser, and some Apple-targeted sites and CDNs serve different content or unlock different endpoints when they see a Safari user agent. Its version is tied to the operating system version, so confirm that both the Safari version and the OS version match a real current release before you use these. As of this writing, that means Safari 26 on macOS 26 (Tahoe) and iOS 26.5.
Two frozen tokens catch people out here. Desktop Safari still reports Intel Mac OS X 10_15_7 even on Apple Silicon and macOS 26. On iOS, the CPU iPhone OS token is frozen to 18_7, while the current Safari release appears in the Version/ field. The string also retains the fixed Mobile/15E148 build identifier and ends with Safari/604.1. Pair these with Safari's own Accept and Accept-Language headers rather than copying Chrome's defaults.
One quirk is worth knowing: modern iPadOS requests desktop sites by default and often identifies itself as a Mac. That is why there is no separate iPad string here. Sites that need to distinguish iPads from Macs rely on signals beyond the User-Agent.
Edge (Chromium) User Agents
Edge has run on Chromium since 2020, so it shares Chromium's Blink engine and even retains the AppleWebKit/537.36 and Safari/537.36 compatibility tokens, with one addition: an Edg/ segment at the end of the string. Its smaller market share makes it a useful rotation supplement that still looks entirely ordinary, and it fits Microsoft properties and enterprise B2B portals well.
Because Edge is the default browser on Windows, it accounts for a meaningful share of everyday web traffic. That makes it a useful addition to a rotation pool: common enough to blend in, yet distinct enough to add a second Chromium identity. Edge is a Chromium browser, so it sends Client Hints. The brand list names Microsoft Edge in place of Google Chrome:
As with Chrome, the Not)A;Brand entry is a GREASE value that changes between releases, so copy it from a current Edge installation instead of hardcoding it.
Mobile User Agents (Android Chrome, iPhone Safari)
Mobile user agents help for two concrete reasons. Many sites serve a lighter page or a JSON-friendly mobile API that is far easier to parse than the desktop markup, and some A/B tests, regional pricing, and paywalls behave differently for mobile visitors, so a mobile string can surface data a desktop string never sees.
The Android string looks oddly generic on purpose: Chrome's User-Agent Reduction freezes the Android version to 10 and the device model to K, then moves the real values into the Sec-CH-UA-Platform-Version and Sec-CH-UA-Model hints, which a site requests only when it needs them. The matching mobile Client Hints for Android Chrome set the mobile flag to ?1:
iOS Safari is not a Chromium browser, so it sends no Client Hints. Send the iOS string on its own with Safari's Accept headers and nothing more.
If a site offers both desktop and mobile experiences, try both. The mobile version is often lighter, easier to parse, and sometimes exposes data or endpoints that the desktop experience does not.
How to Set a User Agent in Python (Code)
Three libraries cover most scraping work: requests for synchronous scripts, httpx for async work, and Selenium or Playwright for browser automation. Every example below verifies the headers with httpbin.org/headers so you can confirm they arrived intact.
Using the requests Library
Set the whole header set, not just the user agent. Sending the User-Agent alone against a full browser header profile is a giveaway; keep the entire header set together so it matches what a real Chrome browser sends.
import requests
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/151.0.0.0 Safari/537.36"
),
"Accept": (
"text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,*/*;q=0.8"
),
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Sec-CH-UA": '"Chromium";v="151", "Google Chrome";v="151", "Not)A;Brand";v="8"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
response = requests.get("https://httpbin.org/headers", headers=headers)
response.raise_for_status()
print(response.json())
Running the script prints the headers that httpbin.org received, letting you verify that the full header set was sent correctly.
Using httpx
The httpx client takes the same header set. One advantage is that httpx supports HTTP/2 natively, bringing it closer to real browser behavior than the HTTP/1.1 requests that requests uses by default.
import httpx
```
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/151.0.0.0 Safari/537.36"
),
"Accept": (
"text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,/;q=0.8"
),
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Sec-CH-UA": '"Chromium";v="151", "Google Chrome";v="151", "Not)A;Brand";v="8"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
}
http2=True allows the client to negotiate HTTP/2 with supporting servers.
with httpx.Client(http2=True, headers=headers) as client:
response = client.get("https://httpbin.org/headers")
response.raise_for_status()
print(response.json())
```
Running the script prints the headers that httpbin.org received, so you can verify that the full header set was sent correctly.
Using Selenium and Playwright
For JavaScript-heavy sites, set the user agent through the browser options before launch so that the HTTP header and navigator.userAgent stay in sync. Setting only the request header leaves the JavaScript value unchanged, and a mismatch between the two is trivial to detect.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
```
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/151.0.0.0 Safari/537.36"
)
options = Options()
options.add_argument(f"--user-agent={UA}")
Hide the most obvious automation flags.
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
driver = webdriver.Chrome(options=options)
driver.get("https://httpbin.org/headers")
print(driver.page_source)
driver.quit()
Playwright sets the user agent and any extra headers on the browser context:
from playwright.sync_api import sync_playwright
UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/151.0.0.0 Safari/537.36"
)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
user_agent=UA,
extra_http_headers={
"Accept-Language": "en-US,en;q=0.9",
"Sec-CH-UA": '"Chromium";v="151", "Google Chrome";v="151", "Not)A;Brand";v="8"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
},
)
page = context.new_page()
page.goto("https://httpbin.org/headers")
print(page.content())
context.close()
browser.close()
```
A correct user agent is necessary but not sufficient for headless work. Sites also check navigator.webdriver, the TLS fingerprint (JA3 or JA4), and behavioral signals, none of which a header touches. Pair a correct user agent with a stealth tool such as playwright-stealth or undetected-chromedriver to cover those layers.
Rotating User Agents the Right Way
Rotation done badly is worse than no rotation, because an incoherent request stands out more than a coherent one. The goal is not maximum variety; it is a stream of requests that each look like a plausible, self-consistent visitor. A pool that swaps user agents at random while leaving the other headers fixed produces impossible combinations no real browser would send, and those combinations are easier to flag than a single honest string repeated. Five rules keep a rotation pool believable:
-
Rotate headers as a set, not just the user agent. Every user agent implies specific Accept, Accept-Language, Accept-Encoding, and Sec-CH-UA values. Mixing a Chrome user agent with a Firefox Accept header is a strong detection signal.
-
Keep a small, fresh pool. Ten to twenty real, current strings beat a large pool of stale ones every time.
-
Rotate per session, not per request. Real users do not switch browsers mid-session, so assign one user agent at session start and keep it for that session.
-
Match the user agent to the proxy location. A Windows Chrome user agent behind a residential IP from Tokyo is coherent; the same string on a datacenter IP in a place with almost no Chrome users is not.
-
Update on a schedule. Browser versions change every four to six weeks, so refresh the list monthly. An outdated major version gets flagged by firewalls like Cloudflare, Akamai, and DataDome.
The snippet below keeps a pool of complete header dictionaries and picks one with random.choice once per session, not once per request:
```
import random
import requests
A small, fresh pool of complete header sets, each internally consistent.
HEADER_POOL = [
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Sec-CH-UA": '"Chromium";v="151", "Google Chrome";v="151", "Not)A;Brand";v="8"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
},
{
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Sec-CH-UA": '"Chromium";v="151", "Google Chrome";v="151", "Not)A;Brand";v="8"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"macOS"',
},
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,/;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br, zstd",
# Firefox sends no Sec-CH-UA, so this set omits it on purpose.
},
]
Pick one set at session start and reuse it for the whole session.
session = requests.Session()
session.headers.update(random.choice(HEADER_POOL))
for path in ("/headers", "/user-agent", "/get"):
print(session.get(f"https://httpbin.org{path}").json())
```
One caution on tooling: convenience libraries such as fake-useragent can serve outdated strings from a bundled dataset, so verify the freshness of anything they hand you before it goes into the pool.
The Managed Alternative: Let the API Handle User Agents
If keeping a fresh pool, matching Client Hints, and tracking browser releases sounds like upkeep you would rather skip, a scraping API does all of it for you. Instead of pinning specific strings, you request a device class, and the API serves a current real browser user agent with matching headers, rotated for you. user_agent_type parameter, which accepts these ten values:
| Value | Serves |
|---|---|
| desktop | Any current desktop browser user agent |
| desktop_chrome | Latest desktop Chrome |
| desktop_firefox | Latest desktop Firefox |
| desktop_safari | Latest desktop Safari |
| desktop_edge | Latest desktop Edge |
| desktop_opera | Latest desktop Opera |
| mobile | Any current mobile user agent |
| mobile_android | Latest Android Chrome |
| mobile_ios | Latest iOS Safari |
| tablet | Any current tablet user agent |
Send a request with source set to universal, the target url, and the device class you want:
import requests
payload = {
"source": "universal",
"url": "https://example.com",
"user_agent_type": "desktop_chrome",
}
response = requests.post(
"https://realtime.oxylabs.io/v1/queries",
auth=("USERNAME", "PASSWORD"),
json=payload,
)
response.raise_for_status()
print(response.json())
The API automatically pairs the user agent with the correct Sec-CH-UA, Accept-Language, and other headers, so there is no manual string maintenance. Whichever approach you choose, keeping browser profiles current and internally consistent is one of the simplest ways to reduce avoidable detection.