---
title: "User Agent List for Web Scraping (2026) and How to Rotate Them"
description: "Current user agent strings verified Sept 2026, how to rotate them with matching headers and Client Hints, and where user agent rotation stops working."
url: https://webscraping.ai/blog/user-agent-rotation-for-web-scraping
markdown_index: https://webscraping.ai/llms.txt
---
# User Agent List for Web Scraping (2026) and How to Rotate Them

Below is a list of real, current user agent strings (verified September 2026), followed by how to rotate them without making your scraper _more_ detectable than it was with the default one. That second part matters more than the list: a random user agent attached to a header set that doesn't match it is a stronger bot signal than `python-requests/2.34.2`, because no real browser has ever sent that combination. This guide covers the strings, the header bundles that must travel with them, what Client Hints (`Sec-CH-UA`) changed, and the point at which rotation stops helping at all.

## Key Takeaways

- Chrome's User-Agent Reduction froze the minor/build/patch version to `0.0.0`, the Android version to `10`, and the device model to `K` — a Chrome UA claiming `Android 14; SM-S918B` is fabricated and trivially flagged
- Rotate **profiles** , not strings: one user agent plus its exact `Accept`, `Accept-Language`, `Sec-CH-UA`, and `Sec-Fetch-*` headers, changed together
- Firefox sends no `Sec-CH-UA` headers at all; a Firefox user agent arriving with them is an instant mismatch
- `requests` speaks only HTTP/1.1, so claiming to be Chrome 154 already contradicts the connection itself — `httpx[http2]` or `curl_cffi` fixes this, plain `requests` cannot
- Keep one user agent per session/IP pair for its lifetime; rotating on every request from one IP is a pattern real users never produce
- Against Cloudflare, DataDome, or PerimeterX, user agent rotation contributes roughly nothing — TLS/JA3 and browser fingerprints decide the outcome

## What does a user agent string look like in 2026?

The `User-Agent` header is one line of text the client sends on every HTTP request, naming the browser, engine, and platform. The important 2026 change is that it carries far less information than tutorials written before 2023 assume.

Chrome's [User-Agent Reduction](https://www.chromium.org/updates/ua-reduction/) completed rollout with Chrome 113 and permanently froze several fields:

| Field | Real value | What Chrome now sends |
| --- | --- | --- |
| Minor/build/patch | `154.0.8037.58` | `154.0.0.0` |
| macOS version | `27.0` | `10_15_7` |
| Windows version | `11` | `Windows NT 10.0; Win64; x64` |
| Android version | `16` | `10` |
| Device model | `Pixel 10 Pro` | `K` |

This is the single most useful freshness check on any user agent list you find: if it offers you Chrome strings with four-part version numbers, real Android versions, or actual phone models, those strings were valid in 2022 and are now anomalies. Most published lists still carry them.

Safari and Firefox freeze less but do freeze: Safari reports `Intel Mac OS X 10_15_7` on every macOS 11+ machine and, since iOS 26, a frozen iOS 18 version (`18_7` at the time of writing) on iPhone, and Firefox has [capped the reported macOS version at 10.15 since Firefox 87](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent/Firefox), reporting Apple Silicon Macs as `Intel`.

## User agent list for web scraping (verified September 2026)

Chrome Stable was 154, Firefox 156, and Safari 27.0 when this list was checked. The version numbers are the only part that decays — the surrounding structure is frozen, so bumping the major number keeps a string valid for the next release.

**Chrome 154 — desktop**

```text
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
```

**Chrome 154 — mobile** (note the frozen `Android 10; K`)

```text
Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Mobile Safari/537.36
```

**Edge 154 — Windows**

```text
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36 Edg/154.0.0.0
```

**Firefox 156**

```text
Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0
Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:156.0) Gecko/20100101 Firefox/156.0
Mozilla/5.0 (X11; Linux x86_64; rv:156.0) Gecko/20100101 Firefox/156.0
Mozilla/5.0 (Android 10; Mobile; rv:156.0) Gecko/156.0 Firefox/156.0
```

**Safari 27**

```text
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Safari/605.1.15
Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/15E148 Safari/604.1
Mozilla/5.0 (iPad; CPU OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/15E148 Safari/604.1
```

A hardcoded list goes stale fast: Chrome has shipped a new major version every two weeks since Chrome 153, and Firefox roughly every four. (iPad Safari also sends the Mac string above by default; the iPad line is its "Request Mobile Website" variant.) Two ways to stay current without maintaining it by hand: read `navigator.userAgent` out of a real browser you already run for JavaScript-heavy pages (see the [headless browser guide](https://webscraping.ai/blog/headless-browser-guide)), or let your scraping API supply the browser identity so the list is somebody else's problem.

The `fake-useragent` package is the common shortcut here. It is fine for what it does — sampling a string from a bundled dataset — but it hands you a bare user agent with no matching headers, and its dataset includes long-tail browsers you do not want to impersonate. Used naively it produces exactly the mismatch described next.

## Why a random user agent is worse than no rotation at all

Browsers are consistent. Chrome 154 on Windows always sends the same `Accept`, the same `Accept-Encoding`, the same `Sec-Fetch-*` set, and a `Sec-CH-UA` whose version agrees with the user agent. Detection systems do not evaluate the user agent in isolation; they check whether the whole request is internally coherent.

So this is a bot signature, not a disguise:

```python
# WRONG: Chrome UA, requests' default everything else
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/154.0.0.0 Safari/537.36"}
requests.get(url, headers=headers)
```

That request claims to be Chrome 154 while sending `Accept: */*`, `Accept-Encoding: gzip, deflate`, no `Sec-CH-UA`, no `Sec-Fetch-*`, and negotiating HTTP/1.1. Chrome does none of those things. A default `python-requests/2.34.2` user agent is at least honest and is only blocked where automation is blocked outright; the half-disguise is affirmatively suspicious.

The rule: **rotate profiles, not strings.** A profile is a user agent plus every header that browser sends with it, and they change together or not at all.

## What Client Hints changed

User-Agent Client Hints are the mechanism replacing the information Chrome removed from the user agent. Chromium browsers send three headers on every request without being asked:

| Header | Example | Sent by |
| --- | --- | --- |
| `Sec-CH-UA` | `"Google Chrome";v="154", "Chromium";v="154", "Not.A/Brand";v="24"` | Chromium only |
| `Sec-CH-UA-Mobile` | `?0` | Chromium only |
| `Sec-CH-UA-Platform` | `"Windows"` | Chromium only |

Higher-entropy hints — `Sec-CH-UA-Full-Version-List`, `Sec-CH-UA-Platform-Version`, `Sec-CH-UA-Arch`, `Sec-CH-UA-Bitness`, `Sec-CH-UA-Model` — are only sent after the server requests them via an `Accept-CH` response header. If a site sends `Accept-CH` and your scraper never responds with the hint on the next request, that silence is itself measurable.

Three practical consequences most scraping content still misses:

1. **A Chrome user agent with no `Sec-CH-UA` is contradictory.** Real Chrome sends it on every secure-context request.
2. **A Firefox or Safari user agent with `Sec-CH-UA` is equally contradictory.** Neither browser implements Client Hints. Copying one "browser header template" across all your profiles creates this bug.
3. **The versions must agree.** `Sec-CH-UA` claiming `v="154"` alongside a `Chrome/131.0.0.0` user agent is a mismatch a single regex catches.

The `"Not.A/Brand"` entry is intentional GREASE — Chrome varies its position, name punctuation, and version specifically so servers can't hardcode the list. Copying one fixed value forever is a weak signal, but it is a signal.

## How to rotate user agents in Python

Keep complete profiles, pick one per session, and hold it for that session's life:

```python
import random
import requests

CHROME_154_WINDOWS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/154.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,application/signed-exchange;v=b3;q=0.7",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Sec-CH-UA": '"Google Chrome";v="154", "Chromium";v="154", "Not.A/Brand";v="24"',
    "Sec-CH-UA-Mobile": "?0",
    "Sec-CH-UA-Platform": '"Windows"',
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
    "Priority": "u=0, i",
}

FIREFOX_156_MAC = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:156.0) "
                  "Gecko/20100101 Firefox/156.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    # No Sec-CH-UA headers — Firefox does not implement Client Hints
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
    "Priority": "u=0, i",
}

PROFILES = [CHROME_154_WINDOWS, FIREFOX_156_MAC]

def new_session():
    """One browser identity per session, held for the session's lifetime."""
    session = requests.Session()
    session.headers.clear() # drop requests' own defaults, which leak
    session.headers.update(random.choice(PROFILES))
    return session

session = new_session()
for url in urls:
    response = session.get(url, timeout=15)
```

`session.headers.clear()` is the line people miss. Without it `requests` merges its own `Accept: */*` and `User-Agent` defaults into every request, and on Python 3.7+ the insertion order also determines the order headers go out on the wire — which browsers keep stable and detection systems do check.

One limit no header dictionary can fix: `requests` is HTTP/1.1 only. Chrome 154 uses HTTP/2 or HTTP/3 for essentially every site it visits, so the protocol contradicts the user agent before any header is read. Use `httpx` with HTTP/2 enabled, or `curl_cffi`, which matches Chrome's TLS handshake as well:

```python
from curl_cffi import requests as cffi_requests

# Matches Chrome's JA3/TLS fingerprint and HTTP/2 settings, not just headers
response = cffi_requests.get("https://example.com", impersonate="chrome")
```

### Node.js

Node's built-in `fetch` (undici) sends headers in the order given and speaks HTTP/1.1 by default:

```javascript
const PROFILES = [
  {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
                  '(KHTML, like Gecko) Chrome/154.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,application/signed-exchange;v=b3;q=0.7',
    'Accept-Language': 'en-US,en;q=0.9',
    'Sec-CH-UA': '"Google Chrome";v="154", "Chromium";v="154", "Not.A/Brand";v="24"',
    'Sec-CH-UA-Mobile': '?0',
    'Sec-CH-UA-Platform': '"Windows"',
    'Sec-Fetch-Dest': 'document',
    'Sec-Fetch-Mode': 'navigate',
    'Sec-Fetch-Site': 'none',
    'Upgrade-Insecure-Requests': '1',
  },
];

const profile = PROFILES[Math.floor(Math.random() * PROFILES.length)];
const response = await fetch(url, { headers: profile });
```

### How often should you rotate?

Bind the identity to the session and the IP, not to the request:

- **One user agent per session** , for the whole session — a real browser never changes mid-visit
- **One user agent per IP** , consistently — five browsers behind one residential IP in a minute is the pattern rotation is supposed to hide
- **Never per-request from a single IP** — this is the most common mistake and it manufactures a signature
- **Weight toward Chrome.** Chrome and Chromium derivatives are the large majority of real desktop traffic; a fleet that is one-third Firefox does not resemble any real audience

## Does user agent rotation still work?

Honestly: on its own, only against the weakest checks. It is table stakes, not a strategy. Here is what each layer actually defeats:

| Technique | Defeats | Does not defeat |
| --- | --- | --- |
| Setting a real user agent | Naive UA blocklists, `403` on `python-requests` | Anything else |
| Full consistent header profile | Header-mismatch heuristics, WAF rules | TLS and browser fingerprinting |
| TLS/JA3 matching (`curl_cffi`) | JA3/JA4 fingerprint checks | JavaScript challenges, behavioral analysis |
| Residential/mobile IPs | IP reputation and ASN blocks | Everything client-side |
| Real browser + stealth patches | Canvas/WebGL/CDP fingerprinting, JS challenges | Behavioral and account-level signals |

A decision rule that holds up in practice: start with a correct header profile over datacenter proxies. If you see `403`s or challenge pages, the problem is almost never the user agent — move to [residential proxies](https://webscraping.ai/blog/best-residential-proxy-providers-for-web-scraping) and TLS matching. If challenge pages persist, you need a real browser, because the site is running JavaScript you have to execute. The [proxy types guide](https://webscraping.ai/blog/types-of-proxies-for-web-scraping) covers where each tier is worth its cost.

Sites behind Cloudflare Bot Management, DataDome, or PerimeterX fingerprint the TLS handshake before your headers are even parsed. No user agent list changes that outcome.

## Rotating headers with the WebScraping.AI API

If you would rather not maintain profiles at all, the API renders pages in a real browser and manages the identity for you. Pass your own headers when you need specific ones — cookies, `Referer`, a language — with the nested `headers[Name]` syntax:

```python
import requests

response = requests.get(
    "https://api.webscraping.ai/html",
    params={
        "api_key": API_KEY,
        "url": "https://example.com/products",
        "proxy": "residential",
        "country": "us",
        "headers[Referer]": "https://www.google.com/",
        "headers[Accept-Language]": "en-US,en;q=0.9",
    },
    timeout=60,
)
print(response.text)
```

Use `device` to switch between the desktop and mobile browser profiles — useful when a site serves a lighter, easier-to-parse page to phones:

```python
params = {
    "api_key": API_KEY,
    "url": "https://example.com/products",
    "device": "mobile",
    "proxy": "residential",
}
```

Proxy tiers map to the escalation ladder above: `datacenter` (1 credit without JS rendering, 5 with) for sites that don't care, `residential` (10 / 25) when datacenter IPs get blocked, and `stealth` (50) for sites running commercial anti-bot systems. AI extraction adds 5 credits. Failed requests are never charged, so probing which tier a site actually requires costs nothing. Full parameter reference is in the [API docs](https://webscraping.ai/docs).

## Frequently Asked Questions

### What is the best user agent for web scraping?

The most recent Chrome release on Windows, because it is the single most common real browser identity and blends into the largest crowd:

```text
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
```

It only works paired with Chrome's matching `Accept`, `Sec-CH-UA`, and `Sec-Fetch-*` headers. Sent alone from an HTTP client, it is worse than no user agent at all.

### Do I need to send `Sec-CH-UA` headers when scraping?

If you claim to be Chrome or Edge, yes — real Chromium sends them on every secure-context request, and their absence contradicts your user agent. If you claim to be Firefox or Safari, you must _not_ send them, because those browsers do not implement Client Hints.

### Is user agent rotation the same as proxy rotation?

No, and they solve different problems. User agent rotation varies the client identity in your headers; [proxy rotation](https://webscraping.ai/blog/types-of-proxies-for-web-scraping) varies the IP the request comes from. Rate limits and IP reputation are enforced on the address, so proxy rotation is what stops volume-based blocks. Rotating user agents from one IP does not raise your request ceiling.

### Is `fake-useragent` still safe to use?

It works for generating strings, but it does not solve the actual problem: it returns a bare user agent with no matching header set, and its dataset includes browsers with negligible real-world share. If you use it, pair each string with a hand-built header profile — at which point maintaining a few explicit profiles is simpler.

### Why does my scraper get blocked even with a valid user agent?

Because the check that blocked you was probably not looking at the user agent. The usual causes, in order: IP reputation (fix with better proxies), TLS/JA3 fingerprint (fix with `curl_cffi` or a real browser), a JavaScript challenge (fix with a [headless browser](https://webscraping.ai/blog/headless-browser-guide) or a rendering API), and request timing. Work down that list before editing your user agent list again.

### Is scraping with a rotated user agent legal?

Sending a `User-Agent` header is not itself a legal question — every HTTP client sends one. What matters is the data, the site's terms, and your jurisdiction. See [is web scraping legal](https://webscraping.ai/blog/is-web-scraping-legal) for the actual considerations, and don't treat rotation as a way around a site that has told you not to scrape it.

* * *

Correct headers get you past naive checks; past that, the identity has to be real all the way down to the TLS handshake. [WebScraping.AI](https://webscraping.ai/) handles browser rendering, header consistency, and proxy rotation behind a single API call — 2,000 free credits per month, no credit card. It's the same engine behind use cases like [price monitoring](https://webscraping.ai/use-cases/price-monitoring), where the target sites do run bot detection.
