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
| Marker | Headless default | Real Chrome value | Detection impact |
|---|---|---|---|
navigator.webdriver | true | undefined | Immediate block on most targets |
window.chrome | Missing / {} | { runtime: {}, loadTimes: fn, ... } | Flags 60%+ of anti-bot systems |
navigator.plugins | Empty array [] | 2–5 plugin objects | Checked 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 behavior | Real browser response | Cloudflare 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.
Building proxy infrastructure since 2019. Previously failed at many things, now failing slightly less.
Related Articles
Puppeteer Proxy Setup: Headless Chrome with Rotating IPs
Configure Puppeteer with rotating residential proxies. Covers proxy authentication, page-level rotation, session persistence, and stealth plugins.
6 min readSelenium Proxy Setup: Complete Guide for Python & Java
Configure rotating residential proxies in Selenium for Python and Java. Covers authentication, geo-targeting, session persistence, and anti-detection patterns.
6 min readContinue exploring
Implementation guides for requests, Scrapy, Axios, Puppeteer, and more.
See how residential proxies fit large-scale scraping workflows.
Evaluate ProxyLabs against Bright Data, Oxylabs, Smartproxy, and others.
Browse location coverage and targeting options across 195+ countries.