Evasion techniques
Detection systems correlate signals across layers: the network fingerprint (TCP/TLS/HTTP2), the browser fingerprint (canvas, WebGL, navigator), and behavior (mouse, keyboard, timing). Passing one layer while failing another still flags you. A residential IP with a mismatched TCP fingerprint, or a perfect browser fingerprint with robotic clicks, gets caught by anything that cross-checks. This page covers what Pydoll gives you for free and the levers you control to keep the layers consistent.
What you get for free
Because Pydoll drives a real Chrome over CDP rather than synthesizing requests, several layers are authentic without any configuration:
- Real network fingerprints. Chrome's TCP/IP stack, TLS (BoringSSL), and HTTP/2 stack produce genuine fingerprints: the TLS ClientHello, the HTTP/2
SETTINGSframe, pseudo-header order, and stream priorities all match a real Chrome. Tools that build requests programmatically (requests, httpx, curl) do not. - Real browser fingerprints. Canvas, WebGL, and AudioContext come from real GPU and audio hardware. Navigator properties, the built-in PDF plugins, and MIME types reflect genuine browser state.
navigator.webdriverisfalse. Selenium, Playwright, and Puppeteer set it totrue. Pydoll launches without automation flags, so it reportsfalse, the same as a normal session. You don't patch it.- Complete input event sequences. Input dispatched through CDP generates the full event chain (
pointermove,pointerdown,mousedown,pointerup,mouseup,click) exactly as a real user would.
The rest of this page is the layers you do control.
Keep the User-Agent consistent
The most common automation tell is a User-Agent that disagrees with itself: the HTTP User-Agent header saying one thing while navigator.userAgent, navigator.platform, and the Client Hints (Sec-CH-UA, Sec-CH-UA-Platform) say another. Setting --user-agent= as a plain Chrome flag changes only the HTTP header and leaves the JavaScript and Client Hints untouched, which is a mismatch a detector reads immediately.
Pydoll fixes this for you. When it sees a --user-agent= argument, it applies Emulation.setUserAgentOverride with the matching platform and full Client Hints metadata, and injects navigator.vendor / navigator.appVersion, so every layer agrees, including in new tabs.
import asyncio
from pydoll.browser.chromium import Chrome
from pydoll.browser.options import ChromiumOptions
async def main():
options = ChromiumOptions()
options.add_argument(
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/130.0.0.0 Safari/537.36'
)
async with Chrome(options=options) as browser:
tab = await browser.start()
await tab.go_to('https://browserleaks.com/javascript')
asyncio.run(main())
Keep the Chrome/<version> in the string equal to the Chrome you actually run; a version you aren't running is itself a mismatch. The override applies to the first tab, tabs from browser.new_tab(), and tabs found via browser.get_opened_tabs().
Match language, timezone, and geolocation to the IP
Behind a proxy, the browser's language, timezone, and location should agree with the IP's country. An IP in Tokyo with Accept-Language: en-US and an America/New_York timezone is a contradiction.
Language is a standalone option:
options = ChromiumOptions()
options.add_argument('--lang=ja-JP')
options.set_accept_languages('ja-JP,ja;q=0.9,en;q=0.8')
This sets both the Accept-Language header and navigator.language / navigator.languages. Timezone and geolocation have to match too, and they need to stay consistent with the User-Agent OS and the IP all at once. Setting them coherently from a single profile is what apply_fingerprint() is for; see Fingerprint Injection.
Stop WebRTC from leaking your IP
WebRTC can reveal the real IP even behind a proxy, through STUN requests that skip the proxy tunnel. Turn on the built-in protection whenever you use a proxy for stealth:
options = ChromiumOptions()
options.webrtc_leak_protection = True # --force-webrtc-ip-handling-policy=disable_non_proxied_udp
Behave like a person
Instant clicks and perfectly regular keystrokes are a behavioral fingerprint. Pass humanize=True to move the cursor along a curved, human-timed path and type with variable rhythm and occasional corrected typos:
field = await tab.find(id='search')
await field.type_text('browser automation', humanize=True)
await field.click(humanize=True)
See Human-like interactions for the timing model and how to tune it.
Look like a used profile
A brand-new profile with no history and every feature disabled looks nothing like a real user's. Pre-populate the profile through browser_preferences (aged timestamps, a matching Chrome version, enabled features), covered in Browser preferences.
Common mistakes
Randomizing everything. A random hardwareConcurrency, deviceMemory, and screen size produce impossible devices. Real machines are constrained: 4 cores with 8 GB RAM and a 1920x1080 screen is plausible; 17 cores with 0.5 GB RAM and a 4K screen is not. Use profiles captured from real browsers, not random values.
Injecting canvas noise. Adding noise to canvas output backfires: detectors sample the fingerprint repeatedly, and a value that changes between reads is itself an automation signal. Pydoll's canvas is authentic and stable; leave it.
Outdated User-Agents. A UA from a Chrome release six months old lacks features and Client Hints the current version has. Stay within the last two or three major versions, and match the binary you run.
Ignoring session behavior. Even with a clean fingerprint, loading 100 pages in a minute, never scrolling, and never idling are anomalies. Add reading delays, vary the pace, and include natural pauses.
Verify your setup
Check your fingerprint against these before running at scale:
| Tool | URL | Tests |
|---|---|---|
| BrowserLeaks | https://browserleaks.com/ |
Canvas, WebGL, fonts, IP, WebRTC, HTTP/2 |
| CreepJS | https://abrahamjuliot.github.io/creepjs/ |
Lie detection, consistency checks |
| Pixelscan | https://pixelscan.net/ |
Bot-detection analysis |
| IPLeak | https://ipleak.net/ |
WebRTC, DNS, IP leaks |
A quick self-check with Pydoll:
result = await tab.execute_script('''
return {
userAgent: navigator.userAgent,
webdriver: navigator.webdriver,
languages: navigator.languages,
plugins: navigator.plugins.length,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
''')
fp = result['result']['result']['value']
assert fp['webdriver'] is False, 'navigator.webdriver should be false'
assert 'HeadlessChrome' not in fp['userAgent'], 'headless leaking in the UA'
What's next
- Fingerprint injection: apply a coherent identity (User-Agent, WebGL, timezone, locale) from one profile.
- Human-like interactions: the behavioral layer in depth.
- Proxies: change and verify your egress IP.
- Fingerprinting (deep dive): the detection theory behind these levers.