Simplescraper
Skip to content

How to spoof a realistic browser fingerprint in Playwright

How to spoof a realistic browser fingerprint in Playwright

Updated 2026-06-25 · 6 min read

If you're running authorized scrapes against a site that fingerprints the browser, you have probably already changed the User-Agent and still seen the session flagged. The reason is usually that the rest of the fingerprint did not move with it: the navigator platform still reads Linux, the WebGL renderer still names a headless GPU, the viewport is the default 1280x720, and the timezone is UTC while the User-Agent claims a Windows machine in New York. An anti-bot script that cross-checks those values sees a contradiction no stock desktop browser would produce.

The fix is to generate one fingerprint where the User-Agent, navigator platform, WebGL strings, viewport, locale, and timezone all describe the same machine, then inject that whole set into the Playwright context before the first page loads. The canonical generator for this is Apify's fingerprint-injector, which builds header and JavaScript fingerprint pairs from a Bayesian network trained on real browser traffic. It takes about 30 lines of Node.js and one package.

Key terms

  • Fingerprint. The combined set of signals a site reads to identify a browser: User-Agent, navigator properties, screen and viewport size, WebGL vendor and renderer, installed plugins, language, and timezone.
  • fingerprint-injector. Apify's library that generates a coherent fingerprint and patches a Playwright or Puppeteer context so the page reports those values.
  • navigator platform. The navigator.platform string a site reads to learn the operating system, for example Win32. It must agree with the operating system named in the User-Agent.
  • Internal consistency. The property that every spoofed signal describes the same machine. A Windows User-Agent next to a Linux platform or a UTC timezone is the contradiction detectors look for.

Here is what the script does:

  • Generate a fingerprint constrained to desktop Chrome on Windows with fingerprint-injector, so the User-Agent, platform, and WebGL strings agree with the Chromium engine you launch.
  • Create the Playwright context with newInjectedContext, which sets the User-Agent, viewport, and accept-language header from that one fingerprint and adds an init script that patches the navigator and WebGL values.
  • Pin the context locale and timezoneId to match the generated language, so navigator.language, Intl timezone, and the accept-language header all describe the same region.
  • Load a fingerprinting test page and read back the reported User-Agent, platform, and timezone to confirm the set holds together.

The complete script

js
// spoof-fingerprint.mjs
import { chromium } from 'playwright'
import { newInjectedContext } from 'fingerprint-injector'

const browser = await chromium.launch({ headless: true })

// Constrain the generator to the same engine you launch (Chromium),
// the same OS the User-Agent will claim (Windows), and one locale.
// This keeps the navigator platform and WebGL strings consistent with Chrome.
const context = await newInjectedContext(browser, {
  fingerprintOptions: {
    browsers: ['chrome'],
    operatingSystems: ['windows'],
    devices: ['desktop'],
    locales: ['en-US']
  },
  // newInjectedContext applies userAgent, viewport, and accept-language
  // from the generated fingerprint. It does NOT set locale or timezoneId,
  // so pin them here to match the locale above.
  newContextOptions: {
    locale: 'en-US',
    timezoneId: 'America/New_York'
  }
})

const page = await context.newPage()

// Load any page, then read the values the browser reports to client JS.
await page.goto('https://httpbin.org/headers', { waitUntil: 'domcontentloaded' })

const report = await page.evaluate(() => ({
  userAgent: navigator.userAgent,
  platform: navigator.platform,
  languages: navigator.languages,
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  viewport: { width: window.innerWidth, height: window.innerHeight }
}))

console.log(report)

await browser.close()
bash
npm install playwright fingerprint-injector
node spoof-fingerprint.mjs

What each step does

Constrain the generator to one engine and OS. fingerprintOptions filters the Bayesian network down to desktop Chrome on Windows. You launch chromium, so a Chrome-on-Windows fingerprint keeps the User-Agent, navigator.platform (Win32), and the WebGL vendor and renderer in the family a real Chromium reports. Leaving the generator unconstrained can hand you a Safari or Firefox fingerprint that contradicts the engine you are actually running.

Create the context with newInjectedContext. This helper generates one fingerprint and applies it in two places at once. It sets the Playwright context userAgent and viewport from the fingerprint's navigator.userAgent and screen dimensions, sets the accept-language header from the generated headers, and adds an init script that patches navigator, screen, and the WebGL getters before any document loads. Playwright cannot change the User-Agent or viewport after a context exists, which is why this runs at context creation.

Pin locale and timezoneId yourself. newInjectedContext sets the accept-language header and navigator.languages, but it does not set the Chromium-level locale or timezoneId. Without those, Intl.DateTimeFormat().resolvedOptions().timeZone returns the host machine's timezone, often UTC on a server, while your User-Agent and headers claim a US English browser. Passing locale: 'en-US' and an America/New_York timezone closes that gap so the language signals and the clock agree.

Read the values back. The page.evaluate block reports the User-Agent, platform, languages, timezone, and viewport the page actually sees. Run it once and confirm the platform reads Win32, the timezone reads America/New_York, and the languages lead with en-US. If any value still describes the host machine, the spoof is not complete for that signal.

Gotchas

  • The User-Agent moves but the platform does not.

    • Issue: Setting only userAgent in newContext leaves navigator.platform, the WebGL renderer, and the plugin list reporting the host machine, so a Windows User-Agent sits next to a Linux x86_64 platform.
    • Fix: use newInjectedContext, which patches navigator, screen, and WebGL from the same fingerprint, rather than passing a User-Agent string to browser.newContext by hand.
  • The timezone gives the server away.

    • Issue: newInjectedContext does not set timezoneId, so Intl reports the host clock, often UTC, while the User-Agent claims a US browser.
    • Fix: pass timezoneId in newContextOptions matched to the locale, for example 'America/New_York' for en-US.
  • An unconstrained generator returns a fingerprint for the wrong engine.

    • Issue: with no fingerprintOptions, the generator can return a Safari or Firefox fingerprint whose User-Agent and navigator.vendor do not match the Chromium you launched.
    • Fix: set browsers: ['chrome'] and operatingSystems: ['windows'] so the generated values stay in the Chromium-on-Windows family.
  • The viewport stays at the default.

    • Issue: a context created without a viewport keeps Playwright's 1280x720 default, a size a fingerprint reader can flag against the claimed device.
    • Fix: newInjectedContext already sets the viewport from the fingerprint's screen; do not override it with a fixed viewport in newContextOptions unless you also adjust the fingerprint.
  • The fingerprint is set per context, not per page.

    • Issue: opening a second page in the same context shares the same fingerprint, so rotating identity per page does not work.
    • Fix: call newInjectedContext again for a fresh context when you want a different fingerprint, and close the old context when you are done with it.
  • headless still leaks through behavior.

Use this when

You have authorization to scrape a site that reads the browser fingerprint, and a plain User-Agent change is not enough because the site cross-checks platform, WebGL, viewport, and timezone for consistency.

Skip this when

The site only checks the IP reputation (rotate through a proxy pool instead); the block is a Cloudflare or Turnstile interactive challenge rather than a passive fingerprint read (see How to bypass Cloudflare in Puppeteer); the target returns full HTML to a bare request (use fetch and skip the browser); or you only need to vary the User-Agent header on HTTP requests with no browser (see How to rotate user agents per request in Node.js).

Skip the code, just get the data

Simplescraper turns any website into structured data in seconds.