Playwright User Agents: How to Set, Change, and Rotate

TL;DR

  • A Playwright user agent is the User-Agent string your browser context sends on every request. If you get it wrong, you risk getting flagged as a bot early.
  • Set it the right way – through browser.newContext(), not the newPage() shortcut – so your whole session shares one consistent identity.
  • Find out why you can't change a user agent mid-session, and how to swap it cleanly with a new context.
  • Cut down clustering signals by rotating your user agent pool, but only rotate per session and keep the rest of your session config – locale, viewport, and headers – consistent.

Introduction

A user agent is just a text string your browser sends with requests, but it has a vital role in headless browser automation.

It tells target websites what browser, rendering engine, operating system, and version you appear to be using. In Playwright, that's important as the user agent is one of the first browser characteristics anti-bot systems inspect when deciding whether your traffic looks like a normal user or an automated script.

Playwright lets you emulate devices and browsers with a userAgent, viewport, locale, timezone, and touch settings, but those defaults are aimed at reliable testing, not stealthy web scraping.

A default headless setup can still raise suspicion long before your script reaches the data you want, especially when headless behavior, client hints, and other headers do not line up cleanly with the browser you claim to be.

In this guide, you'll learn how to set a custom Playwright user agent, how to change it the right way, how to rotate user agents at scale, and how to troubleshoot the common blocks that show up once you move from a local script to production web scrapers.

What is a Playwright user agent?

A Playwright user agent (UA) is the User-Agent value your browser context sends on HTTP requests. The string usually includes the browser family, engine, operating system, and version information. A desktop Chrome example looks like this:

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

That one line encodes several real signals: the operating system and architecture (Windows NT 10.0; Win64; x64), the rendering engine (AppleWebKit, with the KHTML compatibility token), and the browser name and version (Chrome/155.0.0.0).

A Mac example uses Macintosh; Intel Mac OS X 10_15_7 instead of Windows tokens. Chrome freezes that macOS version token, so it stays 10_15_7 even on the latest macOS.

The point is not ensuring you use the exact punctuation, it's that a real user agent needs to match a real browser and operating system combination. Match the version to the browser you actually launch. Run browser.version() and use that major version, since client hints such as Sec-CH-UA report the real engine version whatever your string says.

Playwright can set that value for you through device presets or explicit context options. Its device emulation includes settings like userAgent, screenSize, viewport, and whether touch events are enabled.

As a result, a default Playwright browser context can become a liability when it comes to scraping – the user agent is only one part of the fingerprint, and the default headless browser path still differs from what real browsers expose. Out of the box, headless Chromium announces itself as HeadlessChrome/ instead of Chrome/ – one of the first things a bot check looks for.

Once you treat the Playwright user agent as part of a bigger browser identity rather than a one-off string, you're ready for the next step.

How to set a user agent in Playwright

The standard way to set a user agent in Playwright is through browser.newContext(). Playwright's API exposes a userAgent option on the browser context, which is the cleanest place to define a specific user agent for a session. The same setting is also available in Playwright Test through use: { userAgent: ... }, use options that are then inherited when you create a new browser context from the built-in browser fixture.

import { chromium } from "playwright";

const USER_AGENT =
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36";

const browser = await chromium.launch();
const context = await browser.newContext({
  userAgent: USER_AGENT,
  locale: "en-US",
  viewport: { width: 1366, height: 768 },
  colorScheme: "light",
});

const page = await context.newPage();
await page.goto("https://example.com");

Use this when you want every new page inside the same browser context to present the same browser identity, such as for login flows, account work, cart flows, multi-step automation, and most web scraping sessions.

A realistic setup is a coherent session: the userAgent, locale, viewport, and other headers all describe the same machine.

Setting a user agent at the page level

Playwright also offers a shortcut through browser.newPage(options), but the wording can be misleading. Although it looks like you are setting options on a single page, Playwright actually creates that page inside a brand-new browser context behind the scenes. That means options such as userAgent apply to the new context created for that page, not to an existing page or session.

Playwright explicitly describes browser.newPage() as a convenience API for short snippets and says production code should use browser.newContext() followed by context.newPage() instead.

const browser = await chromium.launch();

const page = await browser.newPage({
  userAgent:
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
});

await page.goto("https://example.com");

That makes it handy for a throwaway script or a single test, but not the best fit when you need shared cookies, storage state, or multiple tabs in one session. Context-level setup is what you want for anything that resembles production.

How to change a user agent in Playwright

You'll trip up here if you're not careful: you cannot cleanly change the user agent on an existing Playwright browser context.

Playwright has a userAgent option when the context is created, but there's no context.setUserAgent() equivalent later on. If you need a new user agent, create a new context and move forward from there. Contexts are isolated by design and do not share cookies or cache with other contexts unless you explicitly carry state across.

import { chromium } from "playwright";

const browser = await chromium.launch();

const shared = {
  locale: "en-US",
  viewport: { width: 1366, height: 768 },
};

async function create(browser, userAgent, storageState) {
  return browser.newContext({
    ...shared,
    userAgent,
    storageState,
  });
}

let context = await create(
  browser,
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
);

let page = await context.newPage();
await page.goto("https://example.com");

// Need a different user agent? Close and recreate.
const state = await context.storageState();
await context.close();

context = await create(
  browser,
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
  state,
);

page = await context.newPage();

That pattern keeps your script predictable. You create a new user agent, keep the rest of the session config stable, and optionally rehydrate cookies with storageState. Just remember that storageState carries auth data and cookies – it does not magically persist your user agent.

Rotating user agents in Playwright

A single static Playwright user agent can work for a quick script, but it becomes fragile once you scale. If every browser instance hits the same target websites with the same User-Agent, the same viewport, and the same timing profile, you create an easy clustering signal for anti-scraping measures. Rotation is the practical answer – not because it makes you invisible, but because it stops you looking identical across every session.

Building a user agent pool

Start with a small pool of realistic, current strings for browsers you actually launch. A Chrome string on a Chromium binary is plausible; a Firefox string on Chromium is not – the TLS handshake, canvas, and WebGL output will still say Chromium. If you need a Firefox or WebKit identity, launch that engine. Playwright supports both.

Outdated or obscure fake user agents are often worse than no rotation because they create an impossible fingerprint. Browser versions move quickly, so refresh the pool whenever Playwright bumps its bundled browsers.

const userAgents = [
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0",
];

function randomUserAgent() {
  return userAgents[Math.floor(Math.random() * userAgents.length)];
}

const browser = await chromium.launch();
const context = await browser.newContext({
  userAgent: randomUserAgent(),
  locale: "en-US",
  viewport: { width: 1366, height: 768 },
});

A good pool favors legitimate browsers and current versions across common operating systems. A bad pool mixes ancient versions, impossible device signatures, and random user agent strings that don't match any real browser characteristics.

Rotation strategies

The safest rotation strategies are boring, but boring is good:

  • Session-based rotation – Pick one user agent per context or account and keep it stable for the full session.
  • Time-based rotation – Rotate after a worker restart or after a fixed batch of jobs, not every request.
  • Weighted randomization – Bias toward common Chrome and Edge desktop strings instead of overusing rare devices.

What's important is consistency. If you claim to be Chrome on Windows, your locale, platform, viewport, and client hints should not suggest something else. If you override only the string and ignore the rest, you can still get flagged.

Headless vs. headed user agent behavior

Even after you rotate user agents, headless browsers can still look odd.

The default headless Chromium path uses a separate chromium-headless-shell, while headed operations use a regular Chromium build. Opting into the newer headless mode via the 'chromium' channel is closer to the real browser, and modern unified headless is much closer to headful Chrome than the older separate implementation.

Spoofing the user agent alone is rarely enough in headless environments. Detection stacks also inspect navigator.webdriver, missing browser features, odd WebGL data, plugins, client hints, and other automation markers. Essentially, a default headless Playwright or Puppeteer browser with markers like HeadlessChrome, navigator.webdriver, or missing browser quirks is easy to identify.

If you're debugging a block, compare headed and headless browser runs against the same site. Use a trace file, inspect request headers, and check what the page sees from navigator.userAgent, navigator.webdriver, and navigator.userAgentData.

Sometimes the fix is as simple as running headless: false during debugging. Other times the fix is to switch to a more authentic browser channel, or a managed browser.

On Browserless, adding &headless=false to the connection URL removes the HeadlessChrome token from the UA, and you can run a full Chrome binary instead of Chromium so the fingerprint matches the string.

How to persist a user agent across Playwright contexts

Playwright contexts are ephemeral by design. They don't share cookies or cache automatically, which is great for tests but not so great when you want a stable scraping identity across multiple contexts.

The practical solution is to persist your configuration in code, then reapply it every time you create a new browser context. storageState can repopulate cookies and localStorage, but you still need to pass the same userAgent again on each new context.

For Playwright Test, the cleanest pattern is a shared config object:

import { defineConfig } from "@playwright/test";

const USER_AGENT =
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36";

export default defineConfig({
  use: {
    userAgent: USER_AGENT,
    locale: "en-US",
    viewport: { width: 1366, height: 768 },
    storageState: "state.json", // saved earlier via context.storageState({ path: 'state.json' })
  },
});

The object use: { userAgent: ... } is inherited when the built-in fixture calls browser.newContext(), making this the easiest way to maintain consistency across tests, retries, and different versions of the same suite.

For library code, use a factory that receives a shared config object and returns each new browser context with the same identity.

If you need that consistency across reconnects, workers, or remote runs, managed sessions help. Browserless's Session API gives you a connect WebSocket URL you pass to chromium.connectOverCDP().

Cookies, localStorage, and cache are stored in an isolated userDataDir and restored on every reconnect for the life of the session's ttl. Check out the persisting state docs for more information.

Because Playwright has no browser.disconnect(), Browserless's reconnect-based Standard Sessions don't apply – use the Session API above, which works with any CDP client.

Why sites block Playwright user agents

Most Playwright blocks come from a mismatch, not just from one bad header.

The common causes are a default headless fingerprint, stale or unrealistic user agent strings, mismatched other headers and client hints, aggressive request volume, or a browser that claims one thing in the User-Agent header and something else everywhere else. However, detection rarely relies on one signal alone.

Here are some common signs you're being blocked:

SymptomLikely causeHow to fix
403 ForbiddenOutdated or unrealistic user agent, mismatched locale or headers, aggressive request rate, or poor IP reputationUse a current UA, align locale and headers, slow requests, and rotate IPs or upgrade proxies
CAPTCHA or JS challengeHeadless signals, automation markers, or suspicious traffic patterns are triggering bot checksReduce headless fingerprints, add stealth tooling, lower request intensity, and use cleaner IPs
Empty page responsesScripts, assets, or API calls are being blocked even though the page loadsInspect failed requests, check client hints and browser signals, and compare headed vs. headless behavior
Silent redirectsThe site is sending you to a block page, login wall, or verification route without showing an obvious errorCheck the final URL and response chain, then fix the underlying fingerprint, session, or auth issue
Session resetsCookies, IP, or browser identity are changing too often, which makes the session look unstableMaintain consistency across the session by keeping the same UA, headers, cookies, and proxy where possible

The practical fixes are usually straightforward: update to a current specific user agent, align other headers and browser features with that UA, avoid stale fake user agents, and add a stealth layer when you are clearly losing on browser fingerprinting rather than request logic.

When you're spending more time tuning fingerprints than shipping data pipelines, a managed browser service is going to be more cost-effective. Start with our /stealth route (a one-line connection-URL change) when you're fighting bot detection, add residential proxies if IP reputation is the problem, and keep the default route for workloads that aren't being blocked.

Top tools for managing Playwright user agents

If you rank tools by reliability rather than novelty, the list looks like this:

  • Manual pools in code. Best when you want control and already track browser versions.
  • Data-backed generators such as user-agents. Better than stale randomizers because the package says its data is updated daily.
  • Stealth layers like playwright-stealth. Useful for patching obvious markers, but not a silver bullet.
  • Managed browser services such as Browserless. Best when user agent management is only one piece of a broader fingerprinting problem.

The tools to avoid are the stale ones. The fake-useragent npm package was last published 8 years ago, which is exactly how you end up sending outdated strings that raise suspicion instead of reducing it. By contrast, user-agents positions itself around frequently updated real-world data, which is a much better fit if you do want a library-driven pool.

Be similarly realistic about stealth plugins. The current playwright-stealth PyPI package describes itself as a proof-of-concept and says you should not expect it to bypass anything beyond the simplest bot detection methods. That makes it useful as one layer in a stack, but not something you should trust on its own for production scraping.

For larger systems, managed browsers are usually the cleanest answer. Browserless enables you to point existing Playwright code at a WebSocket URL, keeps session state across reconnects, and ships dedicated stealth routes with fingerprint mitigations built in.

Start with /stealth; it's the same Playwright code you already run, just against browsers you don't have to host.

It's the same thing you would otherwise build by hand – just tuned, hosted, and easier to operate.

Conclusion

A Playwright user agent is simple to set and surprisingly easy to misuse. The reliable path is to set a realistic user agent at the browser context level, rotate user agents per session instead of per request, maintain consistency with locale, viewport, and client hints, and remember that headless fingerprints involve far more than one header. If your script still gets blocked, treat it as a full-session fingerprint problem, not just a User-Agent problem.

If you're doing this at scale, sign up for Browserless. You keep the Playwright API you already use, while Browserless handles the managed browsers, session persistence, and stealth routes – so you're not rebuilding user agent management every time a site changes its checks.

Playwright user agent FAQs

What is the default user agent in Playwright?

There's not one universal default across every browser. Playwright inherits the underlying browser or device profile unless you override it, and device presets include userAgent as part of the emulation bundle. For default headless Chromium runs, headless behavior can differ because Playwright uses chromium-headless-shell unless you opt into a different channel.

How do I set a custom user agent in Playwright?

Create a browser context with browser.newContext({ userAgent: '...' }), or set use: { userAgent: '...' } in Playwright Test, which makes the custom user agents apply consistently to pages created inside that context.

Can I change the user agent mid-session in Playwright?

Not cleanly on an existing context. The normal approach is to create a new browser context with the new user agent, and optionally reuse cookies or localStorage via storageState.

Why is my Playwright user agent still getting blocked?

Because the user agent is only one signal. Sites also check headless behavior, client hints, navigator.webdriver, browser features, IP reputation, and whether the rest of your headers match the claimed browser.

What is the best user agent to use for Playwright scraping?

The best user agent is a current, realistic user agent that matches the browser you actually launch and the rest of your session config. A common desktop Chrome or Edge string on Windows or macOS is usually safer than obscure mobile or outdated browser versions.

Does changing the user agent in Playwright make my scraper undetectable?

No. It can help you avoid detection when the default fingerprint is too obvious, but it does not fix the rest of the browser fingerprint or behavior. You are making the session less obvious, not risk-free.

How do I rotate user agents in Playwright without getting flagged?

Rotate per session, not per request. Keep a current user agent pool, assign one user agent to each browser context, and maintain consistency with locale, viewport, platform, and client hints so the whole browser identity matches the chosen string.