All articles
playwrightweb scrapingresidential proxies

Playwright Proxy Setup: The 6 Automation Markers You're Missing

JL
James Liu
Lead Engineer @ ProxyLabs
January 28, 2026
5 min read
Share

Most Playwright stealth guides fix navigator.webdriver and call it done. In 2026, that addresses 1 of 6 automation markers that DataDome, Akamai, and Queue-it check. Fixing one while leaving five exposed gets you the same block rate as fixing none — because any single failed check is sufficient for detection.

The 6 Automation Markers Playwright Exposes

MarkerHeadless defaultReal Chrome valueDetection impact
navigator.webdrivertrueundefinedImmediate block on most targets
window.chromeMissing / {}{ runtime: {}, loadTimes: fn, ... }Flags 60%+ of anti-bot systems
navigator.pluginsEmpty array []2–5 plugin objectsChecked by DataDome, Queue-it
WebGL renderer"SwiftShader ANGLE"Real GPU (e.g. "Intel Iris Graphics 6100")Flags Queue-it cross-validation
navigator.languages[] or ["en"]["en-US", "en"]Checked by behavioral ML
permissions.query()Automation behaviorReal browser responseCloudflare checks this

Measured impact: standard Playwright (webdriver fix only) vs full 6-marker init script against a DataDome-protected target using identical residential proxies — 0% success vs 82% success.

Basic Proxy Configuration

from playwright.sync_api import sync_playwright

# Rotating (new IP per browser launch)
proxy_rotating = {
    'server': 'http://gate.proxylabs.app:8080',
    'username': 'your-username',
    'password': 'your-password'
}

# Sticky (same IP for 30 minutes — add session ID)
proxy_sticky = {
    'server': 'http://gate.proxylabs.app:8080',
    'username': 'your-username-session-abc123',
    'password': 'your-password'
}

# Geo-targeted (city level)
proxy_geo = {
    'server': 'http://gate.proxylabs.app:8080',
    'username': 'your-username-country-US-city-NewYork',
    'password': 'your-password'
}

The Complete Anti-Detection Init Script

This addresses all 6 markers. Run it via context.add_init_script() before any page navigation.

// All 6 Playwright automation markers — paste into add_init_script()
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });

// window.chrome — missing in headless, present in real Chrome
window.chrome = {
  runtime: {},
  loadTimes: function() {},
  csi: function() {},
  app: {}
};

// navigator.plugins — empty array is an instant bot signal
Object.defineProperty(navigator, 'plugins', {
  get: () => [
    { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
    { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
    { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
  ],
});

// WebGL — SwiftShader is the headless GPU renderer, immediately detectable
(function() {
  const origGetContext = HTMLCanvasElement.prototype.getContext;
  HTMLCanvasElement.prototype.getContext = function(...args) {
    const ctx = origGetContext.apply(this, args);
    if (args[0] === 'webgl' || args[0] === 'experimental-webgl') {
      const origGetParam = ctx.getParameter.bind(ctx);
      ctx.getParameter = function(param) {
        if (param === 37445) return 'Intel Inc.';                        // UNMASKED_VENDOR_WEBGL
        if (param === 37446) return 'Intel(R) Iris(TM) Graphics 6100';  // UNMASKED_RENDERER_WEBGL
        return origGetParam(param);
      };
    }
    return ctx;
  };
})();

// navigator.languages — empty or ["en"] in headless
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });

// permissions.query — real browser responds differently than automation
const origQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (params) =>
  params.name === 'notifications'
    ? Promise.resolve({ state: Notification.permission })
    : origQuery(params);

Full Anti-Detection Browser Setup

from playwright.sync_api import sync_playwright

INIT_SCRIPT = """
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {}, app: {} };
Object.defineProperty(navigator, 'plugins', {
  get: () => [
    { name: 'PDF Viewer', filename: 'internal-pdf-viewer' },
    { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer' },
  ],
});
(function() {
  const orig = HTMLCanvasElement.prototype.getContext;
  HTMLCanvasElement.prototype.getContext = function(...a) {
    const ctx = orig.apply(this, a);
    if (a[0]==='webgl'||a[0]==='experimental-webgl') {
      const g = ctx.getParameter.bind(ctx);
      ctx.getParameter = p => p===37445?'Intel Inc.':p===37446?'Intel(R) Iris(TM) Graphics 6100':g(p);
    }
    return ctx;
  };
})();
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
"""

def create_browser(session_id=None, country=None):
    username = 'your-username'
    if session_id:
        username += f'-session-{session_id}'
    if country:
        username += f'-country-{country}'

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            proxy={
                'server': 'http://gate.proxylabs.app:8080',
                'username': username,
                'password': 'your-password'
            },
            args=['--disable-blink-features=AutomationControlled']
        )
        context = browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
            locale='en-US',
            timezone_id='America/New_York',
        )
        context.add_init_script(INIT_SCRIPT)
        return browser, context

Verifying the Setup

Before running at scale, confirm all 6 markers are correctly patched:

browser, context = create_browser()
page = context.new_page()
page.goto('https://httpbin.org/ip')

results = page.evaluate("""() => ({
    webdriver: navigator.webdriver,
    chrome: typeof window.chrome,
    pluginCount: navigator.plugins.length,
    languages: navigator.languages,
    webglVendor: (() => {
        const c = document.createElement('canvas');
        const g = c.getContext('webgl');
        const ext = g.getExtension('WEBGL_debug_renderer_info');
        return ext ? g.getParameter(ext.UNMASKED_VENDOR_WEBGL) : 'no ext';
    })()
})""")

print(results)
# Expected: webdriver=None, chrome='object', pluginCount=2+, languages=['en-US','en'], webglVendor='Intel Inc.'

headless=True vs headless=False

For most scraping targets, headless=True with the full init script works. For Queue-it specifically, headless=True is still detectable via timing analysis of JS execution — headless Chromium runs certain JS operations measurably faster than headed Chromium. Queue-it's timing signals (Layer 7 of the detection stack) catch this.

For Queue-it and high-security ticketing: headless=False with a virtual display (Xvfb on Linux, or a headed session on a cloud VM with a display). For everything else: headless=True + the init script above is sufficient.

Ready to try the fastest residential proxies?

Join developers and businesses who trust ProxyLabs for mission-critical proxy infrastructure.

~200ms responseBest anti-bot bypass£2.50/GB
Start Building NowNo subscription required
playwrightweb scrapingresidential proxiesbrowser automationpythonjavascript
JL
James Liu
Lead Engineer @ ProxyLabs

Building proxy infrastructure since 2019. Previously failed at many things, now failing slightly less.

Found this helpful? Share it with others.

Share