Browser Fingerprint and Stealth Configuration
A headless browser executes JavaScript and renders the DOM, which lets it read data that plain HTTP clients never see — but that same runtime environment leaks dozens of signals that mark it as automated. Anti-bot systems read the navigator.webdriver flag, hash the pixels a canvas renders, inspect the WebGL vendor string, probe installed fonts, and check that your User-Agent, viewport, and timezone tell one consistent story. This guide is part of Advanced Scraping Techniques & Anti-Bot Evasion and explains how each surface is measured and how to neutralize it with playwright-stealth and undetected-chromedriver. Apply these techniques only to targets whose robots.txt and terms of service permit automated access.
When to Use Stealth Configuration
Browser stealth addresses detection that happens after a page loads, inside the JavaScript environment — a different layer from the network-level handshake covered in TLS & JA3 Fingerprint Evasion in Python. Reach for it when:
- A site loads fine in your own browser but shows a challenge or empty page under automation, even though the initial HTTP response is a normal
200. - Fingerprint test pages such as
bot.sannysoft.comshow red rows fornavigator.webdriver, a headless User-Agent, or a missing plugins array. - The data is rendered by client-side JavaScript, so a lightweight HTTP client cannot reach it and a real browser is unavoidable.
- You already automate with Using Playwright for Modern Web Automation or Mastering Selenium for Dynamic Websites and need those sessions to survive fingerprint checks.
If the target instead blocks you at the TLS handshake before any page renders, stealth patches will not help — that is the sibling problem solved by TLS impersonation. Robust pipelines combine both layers.
Prerequisites
Use Python 3.10 or newer and install both stealth toolkits plus their browser engines:
pip install playwright playwright-stealth undetected-chromedriver selenium
playwright install chromium
The playwright install chromium step downloads the browser build Playwright drives. undetected-chromedriver uses your locally installed Google Chrome, so make sure a current Chrome is present on the machine. Confirm the toolchain:
python -c "import playwright, undetected_chromedriver; print('stealth deps OK')"
1. Patching the navigator.webdriver Flag
The single most reliable automation tell is navigator.webdriver, which WebDriver-controlled browsers set to true. Detection scripts read it in one line, so masking it is the baseline. With Playwright you inject an init script that runs before any page JavaScript, redefining the property to return undefined:
from playwright.sync_api import sync_playwright
STEALTH_JS = "Object.defineProperty(navigator, 'webdriver', {get: () => undefined});"
def fetch_flag_state(url: str) -> str:
"""Open a page with the webdriver flag patched and return its title."""
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
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/124.0.0.0 Safari/537.36"
),
)
context.add_init_script(STEALTH_JS)
page = context.new_page()
page.goto(url, wait_until="networkidle", timeout=30000)
title = page.title()
browser.close()
return title
if __name__ == "__main__":
print(fetch_flag_state("https://bot.sannysoft.com"))
The --disable-blink-features=AutomationControlled argument stops Chrome from advertising automation at the browser level, and the init script covers the JavaScript property. The Selenium stealth configuration guide applies the equivalent patch for WebDriver-based stacks.
2. Neutralizing Canvas, WebGL, and Font Fingerprints
Beyond flags, sites fingerprint the rendering stack. A canvas fingerprint draws text and shapes off-screen and hashes the resulting pixels; because GPU, driver, and font differences change those pixels, the hash is stable per machine and unusually stable across a fleet of identical headless containers — which is itself suspicious. WebGL exposes the UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL strings, and font enumeration measures text widths to infer installed fonts.
The playwright-stealth package bundles a set of patches covering these surfaces so you do not hand-write each one:
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
def render_with_stealth(url: str) -> str:
"""Load a fingerprint test page with playwright-stealth patches applied."""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1366, "height": 768},
locale="en-US",
timezone_id="America/New_York",
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
),
)
page = context.new_page()
stealth_sync(page)
page.goto(url, wait_until="networkidle", timeout=30000)
html = page.content()
browser.close()
return html
if __name__ == "__main__":
print(len(render_with_stealth("https://bot.sannysoft.com")))
Setting locale and timezone_id alongside the User-Agent keeps the rendering and environment signals coherent — a US English User-Agent paired with a UTC timezone and a non-US WebGL renderer is exactly the kind of inconsistency detectors score.
3. Keeping User-Agent, Viewport, and Headers Consistent
Individual patches fail when the signals contradict each other. A convincing profile fixes the User-Agent, the Sec-Ch-Ua client hints, the viewport, the platform, and the language as one unit. The helper below centralizes a profile so every browser context shares the same identity:
from dataclasses import dataclass, field
@dataclass(frozen=True)
class BrowserProfile:
"""A self-consistent set of fingerprint-facing values."""
user_agent: str = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
viewport: dict[str, int] = field(default_factory=lambda: {"width": 1920, "height": 1080})
locale: str = "en-US"
timezone_id: str = "America/New_York"
platform: str = "Win32"
def extra_headers(self) -> dict[str, str]:
"""Return client-hint headers matching this profile."""
return {
"User-Agent": self.user_agent,
"Accept-Language": "en-US,en;q=0.9",
"Sec-Ch-Ua": '"Chromium";v="124", "Not-A.Brand";v="99"',
"Sec-Ch-Ua-Platform": '"Windows"',
}
if __name__ == "__main__":
profile = BrowserProfile()
print(profile.extra_headers())
Feed the same BrowserProfile into every context you create so a fleet of workers never mixes a Windows User-Agent with a macOS platform string. When you also rotate identities, coordinate it with how to rotate user agents in Python so the User-Agent and the client hints move together.
4. Choosing Between undetected-chromedriver and playwright-stealth
Both tools mask the same surfaces but suit different stacks. undetected-chromedriver patches the ChromeDriver and browser to hide automation from the start and is a fast retrofit for existing Selenium code. playwright-stealth layers patches onto Playwright, keeping Playwright's async model, auto-waiting, and network interception. The detailed head-to-head in undetected-chromedriver vs playwright-stealth compares coverage, maintenance, and speed so you can pick per project.
Performance and Scaling Considerations
Stealth browsers are the most expensive tool in the anti-bot toolkit: each instance carries a full rendering engine, so memory and CPU dominate your budget and concurrency is bounded by RAM long before it is bounded by network. Keep costs down by reusing a single browser process across many contexts rather than launching one browser per page, and by closing contexts promptly to release memory. Block images, media, and fonts through request interception when you only need HTML, which cuts bandwidth and speeds up networkidle. Where a site actually serves its data through a JSON or HTML endpoint, skip the browser entirely and use a TLS-impersonating client, reserving stealth browsing for genuinely script-rendered pages. For large fleets, run headless in containers with realistic viewport sizes and pin Chrome and driver versions so a background update cannot silently break your patches overnight.
Common Errors and Fixes
bot.sannysoft.com still shows webdriver as present. Your init script ran after navigation, or you patched only the JavaScript property and not the browser flag. Add --disable-blink-features=AutomationControlled at launch and register the init script on the context before the first goto.
playwright_stealth raises ImportError: cannot import name 'stealth_sync'. The installed version exposes a different entry point. Check the package's current API with python -c "import playwright_stealth; print(dir(playwright_stealth))" and import the function it lists; some releases use an apply_stealth helper or an async variant.
undetected_chromedriver fails with This version of ChromeDriver only supports Chrome version N. Your local Chrome updated past the driver. Pass version_main= to match your installed Chrome major version, or upgrade the package so it fetches a matching driver.
Headless renders differ from headful and get flagged. The new headless mode still differs subtly. Launch with --headless=new, set an explicit viewport, and spoof the WebGL vendor/renderer through stealth patches so the rendering surface matches a real desktop.
playwright._impl._errors.TimeoutError waiting on networkidle. A long-polling connection or analytics beacon keeps the network busy. Wait on a specific selector with page.wait_for_selector(...) instead of networkidle, which is more reliable on chatty pages.
Frequently Asked Questions
What is the most important fingerprint to fix first?
The navigator.webdriver flag, because it is a single boolean that instantly identifies WebDriver automation and is trivial for any script to read. Mask it with a browser-level flag plus an init script, then move on to canvas, WebGL, and header consistency.
Can stealth configuration defeat Cloudflare Turnstile or reCAPTCHA? No. Stealth reduces heuristic and fingerprint-based detection, but it does not solve cryptographic or interactive challenges. Those require the browser to genuinely execute the challenge or an integrated solving service; see Bypassing Cloudflare and Akamai Protections.
Do I need TLS impersonation if I already use a stealth browser? A real browser produces a genuine browser TLS handshake, so a headful or properly configured headless Chrome already presents a legitimate JA3. You mainly need separate TLS impersonation when you drop down to a lightweight HTTP client for speed, as described in TLS & JA3 Fingerprint Evasion in Python.
Why does my scraper work locally but get blocked in the cloud? Cloud datacenter IPs carry poor reputation, and containerized headless browsers often share identical, telltale fingerprints. Combine stealth patches with residential proxies and varied, self-consistent profiles so each worker looks like a distinct real user rather than one machine cloned many times.
Is headless mode inherently detectable?
It leaks more signals than headful mode — subtle rendering, missing GPU acceleration, and font differences — but the modern --headless=new engine plus canvas and WebGL patches narrows the gap substantially. When a target is especially strict, running headful in a virtual display remains the most faithful option.