Skip to main content

Overview

Zendriver is designed to be undetectable by default, using the Chrome DevTools Protocol instead of Selenium/WebDriver. However, sophisticated bot detection systems use multiple signals. This guide covers configuration options and best practices to maximize stealth.
Zendriver achieves undetectability by controlling Chrome through CDP rather than WebDriver, making it nearly impossible for websites to detect automation through standard WebDriver checks.

Why Zendriver is undetectable

No WebDriver

Uses CDP instead of Selenium/WebDriver, avoiding the navigator.webdriver flag

Real browser

Controls actual Chrome, not a modified or instrumented version

Natural behavior

Supports human-like interactions: mouse movements, typing delays, etc.

Clean profile

Runs with a real Chrome profile without automation artifacts

Basic anti-detection setup

Start with these recommended settings:
import asyncio
import zendriver as zd

async def main():
    browser = await zd.start(
        headless=False,           # Don't use headless mode
        sandbox=True,             # Enable sandbox for isolation
        disable_webrtc=True,      # Prevent WebRTC IP leaks
        disable_webgl=False,      # Keep WebGL enabled (more natural)
    )
    
    page = await browser.get("https://www.browserscan.net/bot-detection")
    await page.save_screenshot("detection_test.png")
    
    await browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

Configuration options

Headless mode

Headless browsers are more easily detected. Use non-headless mode for better stealth.
# Recommended: Non-headless
browser = await zd.start(headless=False)

# Not recommended: Headless (easier to detect)
browser = await zd.start(headless=True)
Headless browsers have different:
  • User agent strings
  • Plugin arrays
  • WebGL vendor/renderer information
  • Browser feature detection results

User agent

Use a custom user agent to match your target browser profile:
browser = await zd.start(
    user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
Leave user_agent=None (default) to use Chrome’s natural user agent. Only customize if you need to match a specific profile.

WebRTC

WebRTC can leak your real IP address even through VPNs:
# Recommended: Disable WebRTC to prevent IP leaks
browser = await zd.start(disable_webrtc=True)

# Keep enabled if your use case requires WebRTC
browser = await zd.start(disable_webrtc=False)

WebGL

WebGL provides GPU information that can be used for fingerprinting:
# Recommended: Keep WebGL enabled for naturalness
browser = await zd.start(disable_webgl=False)

# Disable if you want to hide GPU information
browser = await zd.start(disable_webgl=True)
Disabling WebGL makes your browser more suspicious. Only disable if you have a specific reason.

Sandbox

The Chrome sandbox provides security isolation:
# Recommended: Enable sandbox
browser = await zd.start(sandbox=True)

# Disable only if running as root or in Docker with issues
browser = await zd.start(sandbox=False)
Running as root automatically disables sandbox. This is less secure but sometimes necessary in containers.

Language

Set the browser language to match your target audience:
# Set language (default is "en-US,en;q=0.9")
browser = await zd.start(lang="en-GB,en;q=0.9")

# Spanish
browser = await zd.start(lang="es-ES,es;q=0.9")

# French
browser = await zd.start(lang="fr-FR,fr;q=0.9")

Browser executable

Use a specific Chrome or Brave executable:
# Use Brave browser
browser = await zd.start(browser="brave")

# Use specific Chrome installation
browser = await zd.start(
    browser_executable_path="/usr/bin/google-chrome-stable"
)

Expert mode

Expert mode disables web security and site isolation. Only use for debugging or in controlled environments.
browser = await zd.start(expert=True)
Expert mode enables:
  • --disable-web-security
  • --disable-site-isolation-trials
  • Shadow DOM always in “open” mode
  • Additional debugging features

Advanced configuration

Custom browser arguments

Pass additional Chrome command-line arguments:
browser = await zd.start(
    browser_args=[
        "--disable-blink-features=AutomationControlled",
        "--disable-features=IsolateOrigins,site-per-process",
        "--window-size=1920,1080",
        "--window-position=0,0",
    ]
)

Persistent profiles

Use a persistent user data directory to maintain cookies and login state:
import pathlib

profile_dir = pathlib.Path.home() / ".zendriver" / "profiles" / "default"
profile_dir.mkdir(parents=True, exist_ok=True)

browser = await zd.start(
    user_data_dir=str(profile_dir)
)
Persistent profiles help avoid repeated logins and appear more natural to websites tracking behavior over time.

Extensions

Load Chrome extensions for additional capabilities:
from zendriver.core.config import Config

config = Config()
config.add_extension("/path/to/extension")

browser = await zd.Browser.create(config=config)

Human-like behavior

Beyond configuration, implement human-like interactions:

Natural typing

import random
import asyncio

async def human_type(element, text):
    """Type with random delays between keystrokes."""
    for char in text:
        await element.send_keys(char)
        # Random delay between 50-150ms
        await asyncio.sleep(random.uniform(0.05, 0.15))

# Usage
search_input = await page.select("input[name='q']")
await human_type(search_input, "zendriver automation")

Mouse movements

import random

async def move_mouse_naturally(page, target_x, target_y, steps=20):
    """Move mouse in a curved path to target."""
    start_x, start_y = 100, 100  # Starting position
    
    for i in range(steps):
        # Linear interpolation with random jitter
        progress = i / steps
        x = start_x + (target_x - start_x) * progress + random.uniform(-5, 5)
        y = start_y + (target_y - start_y) * progress + random.uniform(-5, 5)
        
        await page.mouse_move(x, y)
        await asyncio.sleep(0.01)
    
    # Final position
    await page.mouse_move(target_x, target_y)

# Usage
await move_mouse_naturally(page, 500, 300)

Random delays

import random
import asyncio

async def human_delay(min_seconds=1, max_seconds=3):
    """Wait for a random human-like duration."""
    await asyncio.sleep(random.uniform(min_seconds, max_seconds))

# Usage
await page.get("https://example.com")
await human_delay(2, 4)  # Wait 2-4 seconds
await page.scroll_down(300)

Scroll behavior

import random
import asyncio

async def scroll_naturally(page, distance):
    """Scroll in small increments with delays."""
    scroll_per_step = 50
    steps = abs(distance) // scroll_per_step
    
    for _ in range(steps):
        await page.scroll_down(scroll_per_step)
        await asyncio.sleep(random.uniform(0.1, 0.3))

# Usage
await scroll_naturally(page, 800)

Testing anti-detection

Use these sites to test your configuration:

BrowserScan

Comprehensive bot detection tests

Sannysoft

Tests for common automation signals

Are You Headless

Headless browser detection tests

CreepJS

Advanced browser fingerprinting

Example test script

import asyncio
import zendriver as zd

async def test_detection():
    """Test anti-detection on multiple sites."""
    browser = await zd.start(
        headless=False,
        disable_webrtc=True,
    )
    
    sites = [
        ("BrowserScan", "https://www.browserscan.net/bot-detection"),
        ("Sannysoft", "https://bot.sannysoft.com/"),
        ("Are You Headless", "https://arh.antoinevastel.com/bots/areyouheadless"),
    ]
    
    for name, url in sites:
        print(f"Testing {name}...")
        page = await browser.get(url)
        await page.wait(5)  # Wait for tests to complete
        await page.save_screenshot(f"{name.lower().replace(' ', '_')}.png")
        print(f"{name} screenshot saved")
    
    await browser.stop()

if __name__ == "__main__":
    asyncio.run(test_detection())

Common detection vectors

Vector: Detection of chrome.runtime or automation-related extensionsZendriver solution: No automation extensions loaded by default ✓
Vector: Missing plugins, WebGL vendors, canvas fingerprints in headless modeZendriver solution: Use headless=False (default) ✓
Vector: Perfectly straight mouse movements, instant typing, no human varianceZendriver solution: Implement human-like interactions (see examples above)
Vector: Canvas, WebGL, fonts, plugins, screen resolution fingerprintingZendriver solution: Uses real Chrome with natural fingerprint. Use persistent profiles to maintain consistency.
Vector: TLS handshake patterns differ between automation and real browsersZendriver solution: Uses real Chrome’s networking stack ✓

Best practices

1

Don't use headless mode

Headless browsers are easier to detect. Use headless=False unless you have a specific reason.
2

Implement human-like delays

Add random delays between actions. Real users don’t click instantly.
3

Use persistent profiles

Set user_data_dir to maintain cookies and browsing history across sessions.
4

Respect rate limits

Don’t make requests too quickly. Add delays between page loads.
5

Rotate user agents carefully

If rotating user agents, ensure they match your browser version and OS.
6

Handle Cloudflare properly

Use the built-in Cloudflare bypass with realistic delays. See Cloudflare bypass.
7

Test regularly

Run detection tests periodically as websites update their detection methods.

Production checklist

Troubleshooting

  1. Verify you’re using headless=False
  2. Add more delays between actions
  3. Implement human-like mouse and keyboard behavior
  4. Use a persistent profile directory
  5. Test on detection sites to identify specific issues
See the Cloudflare bypass guide for detailed troubleshooting.
Use a persistent user_data_dir to maintain consistent fingerprints across sessions:
browser = await zd.start(user_data_dir="/path/to/profile")

Further reading

Cloudflare bypass

Handle Cloudflare challenges

CDP commands

Advanced browser control with CDP

Docker deployment

Deploy in production with Docker

nodriver

Original project that inspired Zendriver

Build docs developers (and LLMs) love