Playwright Stealth: Anti-Detection Setup for 2026

Playwright Stealth: Anti-Detection Setup for 2026

Playwright is Microsoft’s browser automation framework that supports Chromium, Firefox, and WebKit. While it’s more modern than Selenium and faster than Puppeteer for many tasks, it still gets detected by anti-bot systems out of the box.

This guide covers how to configure Playwright for stealth scraping — patching detection vectors, adding behavioral signals, and integrating proxies for maximum evasion.

Why Playwright Gets Detected

Standard Playwright reveals itself through:

  • navigator.webdriver returns true
  • Missing or incorrect browser runtime objects
  • Playwright-specific properties in the browser context
  • Default headless mode fingerprints
  • Missing or empty navigator.plugins
  • Incorrect navigator.permissions responses
  • Detectable automation flags in the browser arguments

Stealth Setup with Python

Using playwright-stealth

The playwright-stealth package ports Puppeteer Stealth’s evasions to Playwright:

pip install playwright playwright-stealth
playwright install chromium
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

def scrape_stealthily(url):
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=False,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox",
            ]
        )

        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/122.0.0.0 Safari/537.36"
            ),
            locale="en-US",
            timezone_id="America/New_York",
        )

        page = context.new_page()

        # Apply stealth patches
        stealth_sync(page)

        page.goto(url, wait_until="networkidle")

        content = page.content()
        browser.close()
        return content

html = scrape_stealthily("https://bot.sannysoft.com")

Manual Stealth Configuration

For more control, apply evasions manually:

from playwright.sync_api import sync_playwright

def create_stealth_context(playwright):
    browser = playwright.chromium.launch(
        headless=False,
        args=[
            "--disable-blink-features=AutomationControlled",
            "--disable-features=IsolateOrigins,site-per-process",
            "--no-sandbox",
        ]
    )

    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/122.0.0.0 Safari/537.36"
        ),
        locale="en-US",
        timezone_id="America/New_York",
        color_scheme="light",
        java_script_enabled=True,
    )

    # Apply comprehensive stealth patches
    context.add_init_script("""
        // Remove webdriver flag
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });

        // Fix chrome runtime
        window.chrome = {
            runtime: {
                onMessage: { addListener: () => {} },
                sendMessage: () => {}
            },
            loadTimes: () => ({
                commitLoadTime: Date.now() / 1000,
                connectionInfo: "h2",
                finishDocumentLoadTime: Date.now() / 1000 + 0.1,
                finishLoadTime: Date.now() / 1000 + 0.2,
                firstPaintAfterLoadTime: 0,
                firstPaintTime: Date.now() / 1000 + 0.05,
                navigationType: "Other",
                npnNegotiatedProtocol: "h2",
                requestTime: Date.now() / 1000 - 0.5,
                startLoadTime: Date.now() / 1000 - 0.3,
                wasAlternateProtocolAvailable: false,
                wasFetchedViaSpdy: true,
                wasNpnNegotiated: true
            }),
            csi: () => ({
                startE: Date.now(),
                onloadT: Date.now() + 100,
                pageT: Date.now() + 200,
                tran: 15
            })
        };

        // Fix plugins
        Object.defineProperty(navigator, 'plugins', {
            get: () => [
                {
                    name: 'Chrome PDF Plugin',
                    description: 'Portable Document Format',
                    filename: 'internal-pdf-viewer',
                    length: 1
                },
                {
                    name: 'Chrome PDF Viewer',
                    description: '',
                    filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
                    length: 1
                },
                {
                    name: 'Native Client',
                    description: '',
                    filename: 'internal-nacl-plugin',
                    length: 2
                }
            ]
        });

        // Fix languages
        Object.defineProperty(navigator, 'languages', {
            get: () => ['en-US', 'en']
        });

        // Fix permissions
        const originalQuery = window.navigator.permissions.query;
        window.navigator.permissions.query = (parameters) => (
            parameters.name === 'notifications'
                ? Promise.resolve({ state: Notification.permission })
                : originalQuery(parameters)
        );

        // Fix hardware concurrency
        Object.defineProperty(navigator, 'hardwareConcurrency', {
            get: () => 8
        });

        // Fix device memory
        Object.defineProperty(navigator, 'deviceMemory', {
            get: () => 8
        });

        // Fix platform
        Object.defineProperty(navigator, 'platform', {
            get: () => 'Win32'
        });
    """)

    return browser, context

with sync_playwright() as p:
    browser, context = create_stealth_context(p)
    page = context.new_page()

    page.goto("https://target-site.com")
    print(page.title())

    browser.close()

Node.js Setup

const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth');

// playwright-extra supports puppeteer-extra plugins
chromium.use(stealth());

(async () => {
    const browser = await chromium.launch({
        headless: false,
    });

    const context = await browser.newContext({
        viewport: { width: 1920, height: 1080 },
        userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
    });

    const page = await context.newPage();
    await page.goto('https://bot.sannysoft.com');

    await page.screenshot({ path: 'test.png' });
    await browser.close();
})();

Proxy Integration

Basic Proxy

from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=False,
        proxy={
            "server": "http://residential.example.com:7777",
            "username": "user",
            "password": "pass"
        }
    )

    page = browser.new_page()
    stealth_sync(page)

    page.goto("https://target-site.com")
    print(page.title())
    browser.close()

Per-Context Proxy Rotation

from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

proxies = [
    {"server": "http://gate.example.com:7777", "username": "user1", "password": "pass1"},
    {"server": "http://gate.example.com:7778", "username": "user2", "password": "pass2"},
    {"server": "http://gate.example.com:7779", "username": "user3", "password": "pass3"},
]

with sync_playwright() as p:
    for i, proxy in enumerate(proxies):
        browser = p.chromium.launch(headless=False, proxy=proxy)
        page = browser.new_page()
        stealth_sync(page)

        page.goto("https://httpbin.org/ip")
        ip_text = page.inner_text("body")
        print(f"Proxy {i}: {ip_text}")

        browser.close()

Bypassing Cloudflare

from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
import time

def bypass_cloudflare(url, proxy=None):
    with sync_playwright() as p:
        launch_args = {"headless": False}
        if proxy:
            launch_args["proxy"] = proxy

        browser = p.chromium.launch(**launch_args)

        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/122.0.0.0 Safari/537.36"
            ),
        )

        page = context.new_page()
        stealth_sync(page)

        page.goto(url, wait_until="domcontentloaded")

        # Wait for Cloudflare to resolve
        for _ in range(20):
            if "Checking your browser" not in page.content():
                break
            time.sleep(1)

        # Extract session data
        cookies = context.cookies()
        content = page.content()

        cf_clearance = next(
            (c for c in cookies if c["name"] == "cf_clearance"),
            None
        )

        browser.close()

        return {
            "html": content,
            "cookies": cookies,
            "cf_clearance": cf_clearance["value"] if cf_clearance else None
        }

result = bypass_cloudflare(
    "https://protected-site.com",
    proxy={
        "server": "http://residential.example.com:7777",
        "username": "user",
        "password": "pass"
    }
)

print(f"Content length: {len(result['html'])}")

Human Behavior Simulation

from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
import random
import time

def simulate_human(page):
    """Add human-like behavior to a Playwright page."""

    # Random mouse movements with natural curves
    for _ in range(random.randint(3, 7)):
        x = random.randint(100, 1500)
        y = random.randint(100, 800)
        # Move in steps for more natural movement
        page.mouse.move(x, y, steps=random.randint(5, 15))
        time.sleep(random.uniform(0.1, 0.4))

    # Natural scroll
    scroll_amount = random.randint(200, 600)
    page.mouse.wheel(0, scroll_amount)
    time.sleep(random.uniform(0.5, 1.5))

    # Sometimes scroll back up a bit
    if random.random() > 0.5:
        page.mouse.wheel(0, -random.randint(50, 150))
        time.sleep(random.uniform(0.3, 0.8))

def type_humanly(page, selector, text):
    """Type text with human-like delays."""
    page.click(selector)
    time.sleep(random.uniform(0.2, 0.5))

    for char in text:
        page.keyboard.type(char)
        time.sleep(random.uniform(0.05, 0.15))

Using Firefox for Stealth

Playwright’s Firefox support can bypass some detections that specifically target Chrome:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.firefox.launch(headless=False)

    context = browser.new_context(
        viewport={"width": 1920, "height": 1080},
        locale="en-US",
    )

    page = context.new_page()

    # Firefox doesn't need as many patches as Chromium
    # But still remove webdriver flag
    page.add_init_script("""
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
    """)

    page.goto("https://target-site.com")
    print(page.title())
    browser.close()

Playwright vs Puppeteer vs Undetected ChromeDriver

FeaturePlaywright + StealthPuppeteer + StealthUndetected ChromeDriver
Multi-browserChromium, Firefox, WebKitChromium onlyChrome only
LanguagesPython, JS, Java, C#JavaScriptPython
Auto-waitYesManualManual
Network interceptionExcellentGoodBasic
Stealth qualityGood (with patches)Good (mature plugin)Excellent (binary patching)
SpeedFastFastModerate
Best forMulti-language teamsJS developersPython + Selenium users

For Python projects, Playwright with stealth patches is typically the best choice. For the toughest anti-bot systems, Undetected ChromeDriver may be more effective due to its binary-level patching.

FAQ

Is Playwright Stealth as good as Puppeteer Stealth?

The playwright-stealth Python package ports most of Puppeteer Stealth’s evasions. For Node.js, you can use playwright-extra with the original Puppeteer stealth plugin. In practice, both achieve similar bypass rates. The main difference is maturity — Puppeteer’s stealth plugin has been around longer and is more battle-tested.

Can Playwright run in headed mode on a server?

Yes. Use Xvfb (X Virtual Framebuffer) to create a virtual display on headless servers:

xvfb-run python your_script.py

Or use pyvirtualdisplay in Python for programmatic control.

Which browser engine is best for stealth?

Chromium has the most anti-detection tooling but is also the most targeted by bot detection. Firefox is less commonly used in automation, so some detection systems have weaker Firefox fingerprinting. WebKit (Safari) is the least detectable but has the smallest feature set and worst proxy support.

Does Playwright support SOCKS5 proxies?

Yes. Set the proxy server to socks5://host:port in the launch options. Playwright handles both SOCKS4 and SOCKS5 natively.

How do I handle CAPTCHAs with Playwright?

For reCAPTCHA and hCaptcha, use CAPTCHA solving services to get tokens, then inject them via page.evaluate(). For Cloudflare Turnstile, browser automation with stealth often resolves it automatically. See our Turnstile bypass guide.

Conclusion

Playwright with stealth configuration is a powerful combination for web scraping in 2026. Its multi-browser support, excellent Python and JavaScript APIs, and built-in auto-waiting make it the most developer-friendly browser automation tool available. Pair it with residential proxies and human-like behavior simulation for maximum evasion success.

Useful Resources


Related Reading

Scroll to Top