TL;DR
- OS emulation makes a browser session report one consistent operating system across its
User-Agent,navigator.platform, GPU renderer, fonts and other fingerprint signals, so they don't contradict each other. - Add
emulationOs=windows,macos,linuxorandroidto a Browserless stealth, BrowserQL, unblock or agent connection URL when the session starts. - Values are exact-match lowercase. A typo like
Windowsisn't rejected – it's silently ignored, and the session keeps its default Linux identity. - Your timezone and locale follow your proxy, not the emulated OS. Pair OS emulation with a region-matched proxy when those signals need to agree too.
Introduction
Without OS emulation, a scraper can claim to run on Windows while its GPU renderer and fonts still come from a Linux server. Bot-detection systems compare those signals, and a mismatch is more suspicious than leaving the default identity alone. Overriding the User-Agent yourself only changes one string: the WebGL renderer and installed fonts still describe the host machine.
In this guide, you'll turn on Browserless OS emulation with one connection parameter and verify it from three directions: BrowserQL for a raw identity check, Playwright for an existing Chrome DevTools Protocol (CDP) automation, and the Browser Automation Protocol (BAP) for a page API backed by BrowserQL. Each path matches an example in the companion repository.
Here's the full comparison and demo:
What OS emulation actually changes
OS emulation is a session-level setting that makes every OS-derived browser signal report the same operating system. When you pick an operating system at session creation, you get a User-Agent, User-Agent Client Hints (UA-CH), navigator.platform, GPU renderer, CPU profile, fonts and voices that all describe it. With android, you also get a mobile viewport, pixel ratio, touch input and motion sensors. Android emulation runs on the Chromium and Brave stealth engines.
You turn it on with the emulationOs connection query parameter. It isn't a header or a BrowserQL mutation, so Browserless applies it while provisioning the session and every page starts with the same identity. Supported values are windows, macos, linux and android, in exact lowercase.
The parameter works on stealth routes (/stealth, /chromium/stealth, /chrome/stealth), BrowserQL routes (/chromium/bql, /chrome/bql, /stealth/bql), the unblock and Smart Scrape REST APIs, and the agent route (/chromium/agent). On a plain /chromium or /chrome connection, it's ignored rather than rejected, so the session keeps its default Linux identity without an error.
To pin a particular Android model, add an emulatedDevice slug from the supported devices list alongside emulationOs=android. Without one, you get a real device profile picked for the session.
Step 1: Get the runnable examples
You need Node.js 20.17+ or 22.9+ with npm 11.10 or newer (the BAP SDK requires that npm) and a Browserless API token. The BrowserQL shell example also needs jq.
Clone the official examples and install the dependencies:
git clone https://github.com/browserless/video-examples.git
cd video-examples/os-emulation
npm install
cp .env.example .env
Open .env and set BROWSERLESS_TOKEN. You can also set BROWSERLESS_BASE if you want a different Browserless region or a self-hosted instance.
The folder contains three independent examples. They're listed in the order this guide uses them, which differs from the file numbering:
- BrowserQL –
1-bql-os-emulation.graphqlandrun-1-bql.shtest the browser identity against a detection page (Step 2). - Playwright –
3-playwright-baas.mjscaptures a responsive page as Windows and Android over Playwright's CDP connection (Step 3). - BAP –
2-bap-os-emulation.mjsruns the same capture through BAP (Step 4).
Step 2: Verify a coherent identity with BrowserQL
Run the BrowserQL example with one of the supported values:
./run-1-bql.sh windows
The script posts this mutation to the BrowserQL OS emulation endpoint, /chromium/bql?emulationOs=windows:
mutation OsEmulation {
goto(url: "https://bot.sannysoft.com", waitUntil: firstMeaningfulPaint) {
status
}
identity: evaluate(
content: "JSON.stringify({ os: navigator.userAgentData.platform, ua: navigator.userAgent, gpu: (g => g.getParameter(g.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL))(document.createElement('canvas').getContext('webgl')) })"
) {
value
}
screenshot(fullPage: true, type: png) {
base64
}
}
When it works, the command prints the identity the detection page saw and saves bql-windows.png. A Windows session reports "os": "Windows" with a gpu renderer string that names Direct3D. Running it with macos shifts those to "macOS" and a Metal renderer.
The exact GPU model can rotate between sessions. What matters is that the renderer's graphics API agrees with the requested operating system.
The script checks the value before it sends the request, so ./run-1-bql.sh Windows fails straight away. The API itself doesn't do that check, as the failure modes below explain.
Step 3: Add OS emulation to Playwright
If you already use Playwright, add the parameter to a Browserless stealth endpoint (the Playwright stealth guide covers the wider setup). The complete example below loads a responsive product listing as a Windows desktop and logs the identity the page sees:
import { chromium } from "playwright-core";
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error("Set BROWSERLESS_TOKEN first");
const endpoint =
`wss://production-sfo.browserless.io/chromium/stealth` +
`?token=${token}&emulationOs=windows`;
const browser = await chromium.connectOverCDP(endpoint);
try {
const context = browser.contexts()[0];
const page = context?.pages()[0];
if (!page) throw new Error("Browserless did not provision a page");
await page.goto("https://scraping-sandbox.netlify.app/products", {
waitUntil: "load",
});
console.log(
await page.evaluate(() => ({
platform: navigator.platform,
width: window.innerWidth,
pixelRatio: window.devicePixelRatio,
touchPoints: navigator.maxTouchPoints,
})),
);
await page.screenshot({ path: "windows-products.png" });
} finally {
await browser.close();
}
The repository's 3-playwright-baas.mjs runs the same capture for Windows and Android. To try this version on its own, save it as playwright-os-emulation.mjs in the os-emulation folder, so it can use the playwright-core you installed in Step 1, and run it:
BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE node playwright-os-emulation.mjs
You'll see a desktop-width viewport and Win32 in the logged identity, followed by a windows-products.png screenshot. Work in the default context (browser.contexts()[0]), as the example does. New pages and windows in the session keep the emulated identity. A context you create with browser.newContext(), however, doesn't inherit launch-level settings such as proxies.
Change only the query parameter to test another identity:
emulationOs=macos
emulationOs=linux
emulationOs=android
With android, the same page renders at a narrow mobile width with touch points and a high device-pixel ratio. Metrics can differ between runs because each session gets a real device profile, so add emulatedDevice when you need them repeatable.
Step 4: Use BAP when stealth is the priority
BAP gives you a familiar page API while sending BrowserQL operations over one WebSocket. It connects only to /bql endpoints, so every BAP session runs on a stealth-hardened route.
Run the repository's BAP example:
node 2-bap-os-emulation.mjs
It opens two independent sessions. Simplified from the script, the core of each one is:
const endpoint =
`wss://production-sfo.browserless.io/chromium/bql` + `?emulationOs=${emulationOs}`;
const browser = Browserless.connect({
browserWSEndpoint: endpoint,
token: process.env.BROWSERLESS_TOKEN,
});
try {
const page = await browser.newPage();
await page.goto("https://scraping-sandbox.netlify.app/products");
await page.screenshot({ path: `${label}.png`, type: "png" });
} finally {
await browser.close();
}
The command writes a Windows desktop screenshot and an Android mobile screenshot. It also prints each session's platform, viewport width, device-pixel ratio and touch-point count, so you can confirm the requested profile reached the page.
Use BAP when its API covers the job and you want BrowserQL's stealth behavior without writing GraphQL. Keep Playwright or Puppeteer when you need library APIs that BAP doesn't expose.
Step 5: Align location signals when they matter
OS emulation changes OS-derived browser signals only. Timezone and locale come from your proxy setup instead, and the browser language only matches the proxy country when you add proxyLocaleMatch.
If a site expects the browser and IP address to come from the same region, add a matching proxy configuration to the connection URL. For example:
wss://production-sfo.browserless.io/chromium/stealth?token=YOUR_API_TOKEN_HERE&emulationOs=windows&proxy=residential&proxyCountry=us&proxyLocaleMatch=true
The session keeps its Windows identity while traffic routes through the US and the browser locale matches the proxy. Choose the country your workflow actually needs. On protected sites, a timezone or locale that doesn't fit the identity is a detection signal of its own.
Common failure modes
The OS value is silently ignored
Values are exact-match and lowercase. Windows, MacOS and android-phone aren't rejected: the API ignores them, and the session keeps its default Linux identity with no error to catch. Read navigator.platform back after connecting to confirm you got the identity you asked for. The example script validates the value before the request, but your own code won't unless you add the same check.
The session still looks like Linux
Check the route first, then check that emulationOs survived into the final connection URL. It's easy to drop when you build the URL by concatenation, and it has no effect in a header or launch option.
Android doesn't use the device you expected
Without emulatedDevice, you get a device profile picked for the session. Copy a slug exactly from the supported devices list when viewport and hardware metrics must be repeatable: slugs are case-sensitive, and an unknown one silently falls back to an automatic pick. Sending emulatedDevice without emulationOs=android returns an HTTP 400.
A site still blocks the session
A coherent OS identity removes one common fingerprint mismatch. It isn't a universal bypass, and it doesn't change a site's terms of service, so keep your scraping within them. Confirm that IP reputation and request behavior fit the workflow too, alongside the location signals from Step 5. For hard targets, use /stealth or /stealth/bql and add a region-matched residential proxy.
Run the examples, then move one parameter into your scraper
Start with the official OS-emulation examples so you can see the fingerprint and viewport change in isolation. Once the output matches the requested OS, carry the same emulationOs parameter into the stealth connection URL your real scraper uses. Your application code stays the same while the browser identity changes as one coherent unit.
Sign up for a free Browserless account, grab your API token and run the examples.
OS emulation FAQs
Is OS emulation the same as changing the user agent?
No. A User-Agent override changes one string, while OS emulation also aligns the client hints, navigator.platform, GPU renderer and fonts, so detection scripts that cross-check them see one consistent machine.
Can I use OS emulation with Puppeteer?
Yes. Add emulationOs to the stealth connection URL you pass to puppeteer.connect({ browserWSEndpoint }), the same way the Playwright example does.
Does OS emulation work on every Browserless region?
Yes. emulationOs works on any regional endpoint, so you can keep it in the connection URL when you switch BROWSERLESS_BASE to another region.
Can I emulate an iPhone or Safari?
No. The supported identities are Windows, macOS, Linux and Android, and Android emulation presents as mobile Chrome.