Bypass DataDome with Python: The Complete 2026 Guide
TL;DR
- What DataDome actually is, and why it’s genuinely hard to beat
- The 5 detection layers DataDome uses against your scraper
- Code-first techniques: TLS impersonation, stealth browsers, proxy rotation, behavioral mimicry
- A full Mermaid architecture diagram of the bypass pipeline
- When building your own stack stops making sense
1. The Problem With DataDome Specifically
You’ve bypassed rate limits before. This time feels different — and it is.
DataDome isn’t a simple firewall you can header-spoof past. It’s a real-time AI scoring engine used by over 1,200 companies worldwide, including major European e-commerce platforms, ticketing sites, and marketplaces (DataDome.co).
It processes three trillion signals per day to decide whether you’re human.
Your clean Python scraper will fail on the very first request. Before understanding the fix, you need to understand the fight.
2. How DataDome Actually Detects You
DataDome doesn’t flip a binary “bot or not” switch. It builds a trust score in real time across five simultaneous detection layers (Scrapfly, 2026).
Miss one layer and the score drops below the threshold. That’s when you get a 403, a slider CAPTCHA, or silent empty HTML.
The five layers, in order of evaluation speed:
Layer What’s Checked DataDome’s Method TLS Fingerprint Cipher suite, extension order, JA3 hash JA3/JA4 hashing at handshake IP Reputation ASN type, abuse history, datacenter ranges Real-time IP database lookup HTTP Details Protocol version, header order, missing fields HTTP/1.1 vs HTTP/2 analysis Browser Fingerprint Canvas, WebGL, navigator.webdriver, plugins JavaScript execution engine Behavioral Analysis Mouse curves, scroll rhythm, navigation flow ML models on session patterns
The trust score is not static. It recalculates continuously during your session. A scraper that passes the first three checks can still get blocked on page 3 if its scroll behavior looks robotic.
3. Identifying a DataDome Block
Before building a bypass, confirm you’re actually dealing with DataDome. The tell-tale signs are consistent:
- HTTP
403 Forbiddenon first or early requests - A
datadomekey in theSet-Cookieresponse header - An
x-datadome-cidfield in response headers - A slider CAPTCHA page loading instead of site content
- The string
ddappearing in a script tag in the blocked response HTML
A quick check with curl reveals everything:
curl -I https://www.target-site.com
# Look for: set-cookie: datadome=...
# Or: x-datadome-cid: ...
Once confirmed, you know which layers you’re fighting. Now, let’s fight them one by one.
4. Layer 1 — TLS Fingerprinting: The Invisible First Strike
This is DataDome’s fastest and most brutal check. It fires before your headers even arrive.
When Python’s requests library connects over HTTPS, the TLS handshake exposes a JA3 hash — a fingerprint of your cipher suite order, extension list, and protocol version. The JA3 hash for requests/urllib3 looks nothing like Chrome's (DataDome Engineering, 2022).
DataDome maintains a database of known bot fingerprints. Your request gets matched in milliseconds.
The fix: curl_cffi — a Python library that wraps curl-impersonate and replicates Chrome's exact TLS handshake.
# pip install curl_cffi
from curl_cffi import requests
session = requests.Session(impersonate="chrome")
response = session.get("https://target-site.com")
print(response.status_code) # Should be 200, not 403
The impersonate="chrome" parameter doesn't just spoof a header. It modifies the entire TLS handshake — cipher suite ordering, extension values, HTTP/2 frame settings — to match Chrome 131+ exactly.
Supported impersonation targets include
chrome,chrome131,safari,safari_ios, andedge101. The library updates these fingerprints as browsers release new versions.
This single change resolves a significant chunk of DataDome blocks. But it’s only layer one.
5. Layer 2 — HTTP Headers: The Bot’s Loudest Tell
Python’s default requests User-Agent is python-requests/2.31.0. Every DataDome-protected site blocks this instantly. But the User-Agent isn't even the main problem.
DataDome checks header completeness and order as a package. Chrome sends 15+ structured headers in a browser-specific order. Missing Sec-Fetch-Dest, sending headers in the wrong sequence, or having a mismatched Accept-Language vs. proxy geolocation all tank your trust score.
from curl_cffi import requests
CHROME_HEADERS = {
"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",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Sec-CH-UA": '"Not A(Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"',
"Sec-CH-UA-Mobile": "?0",
"Sec-CH-UA-Platform": '"Windows"',
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
}
session = requests.Session(impersonate="chrome")
response = session.get("https://target-site.com", headers=CHROME_HEADERS)
Critical geo-coherence rule: if your proxy is French, your Accept-Language must be fr-FR,fr;q=0.9. A German IP paired with an en-US language header is a trivial mismatch for DataDome's models to catch.
Also: most scraping libraries still default to HTTP/1.1. DataDome flags this. Modern websites run HTTP/2 or HTTP/3. Using curl_cffi or httpx with HTTP/2 enabled solves this automatically.
6. Layer 3 — IP Reputation: The Trust Score Foundation
DataDome cross-references every connecting IP against multiple threat intelligence databases in real time. The verdict is instant and harsh for datacenter IPs.
IP reputation accounts for an estimated 25–30% of the trust score. You can have perfect headers and a flawless TLS fingerprint and still get blocked because your IP belongs to AWS us-east-1.
The three IP tiers and their trust impact:
Datacenter IPs (AWS, GCP, DigitalOcean) → Immediate negative score, pre-blocked ranges
Residential IPs (ISP-assigned, real homes) → High trust, harder to abuse at scale
Mobile IPs (carrier-grade NAT) → Highest trust, shared ranges hard to block
Proxy rotation with residential or mobile IPs is non-negotiable for DataDome. Here’s a clean implementation:
from curl_cffi import requests
import random
RESIDENTIAL_PROXIES = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
def get_proxy() -> dict:
proxy = random.choice(RESIDENTIAL_PROXIES)
return {"http": proxy, "https": proxy}
session = requests.Session(impersonate="chrome")
response = session.get(
"https://target-site.com",
headers=CHROME_HEADERS,
proxies=get_proxy(),
timeout=15
)
⚠️ Sticky sessions matter: never rotate IPs mid-session for multi-step flows. A login → browse → checkout flow that jumps IP addresses mid-stream is an enormous red flag for DataDome’s behavioral layer.
Use sticky sessions for multi-page flows. Rotate only at the start of a new independent session.
7. Layer 4 — Browser Fingerprinting: The JavaScript Interrogation
For sites that render content with JavaScript (which DataDome-protected sites almost always do), you need a real browser. But vanilla Playwright and Selenium announce themselves immediately.
Default headless browsers leak bot signals everywhere (Kameleo, 2025):
navigator.webdriver === true— hardcoded automation flag- Missing plugins (
Chrome PDF Viewer,Google Docs Offline) - Absent
window.chrome.runtimeobject - SwiftShader software GPU rendering instead of real GPU output
- Canvas/WebGL producing slightly different pixel outputs than real browsers
The fix: playwright-stealth combined with manual property patching:
# pip install playwright playwright-stealth
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
async def stealth_scrape(url: str) -> str:
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=["--disable-blink-features=AutomationControlled"]
)
context = await 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",
)
page = await context.new_page()
# Apply stealth patches (200+ leak fixes)
await stealth_async(page)
# Additional manual patching for DataDome-specific checks
await page.add_init_script("""
Object.defineProperty(navigator, 'platform', {
get: () => 'Win32'
});
Object.defineProperty(navigator, 'plugins', {
get: () => [
{ name: 'Chrome PDF Plugin' },
{ name: 'Chrome PDF Viewer' },
{ name: 'Native Client' }
]
});
""")
await page.goto(url, wait_until="networkidle")
content = await page.content()
await browser.close()
return content
html = asyncio.run(stealth_scrape("https://target-site.com"))
For Python-first developers who want even stronger evasion, camoufox — a custom-patched Firefox build — and undetected-chromedriver for Selenium are worth keeping in your toolkit.
8. Layer 5 — Behavioral Analysis: The Hardest Layer
Passing all four previous layers still isn’t enough for sustained scraping. DataDome’s ML models study how you behave across the entire session.
Signals being monitored:
- Mouse movement: real users use Bézier curves with natural acceleration and jitter
- Scroll behavior: irregular bursts with pauses, not linear pixel increments
- Request timing: humans take 2–15 seconds between actions, not 50ms
- Navigation flow: real users visit homepage → category → product, not deep-linked pages directly
- Session warm-up: going straight to a product URL is suspicious; browsing first is natural
import asyncio
import random
async def human_delay(min_s: float = 1.5, max_s: float = 4.0):
"""Randomized delay with realistic variance - not wild swings."""
await asyncio.sleep(random.uniform(min_s, max_s))
async def scroll_naturally(page):
"""Scroll in bursts, pause between them, like a real reader."""
chunks = random.randint(3, 6)
for _ in range(chunks):
scroll_px = random.randint(80, 250)
await page.mouse.wheel(0, scroll_px)
await asyncio.sleep(random.uniform(0.4, 1.3))
async def warm_up_session(page, base_url: str):
"""
Visit homepage and browse naturally before hitting target.
DataDome rewards sessions that 'feel' like real shopping journeys.
"""
await page.goto(base_url, wait_until="domcontentloaded")
await human_delay(2.0, 4.0)
await scroll_naturally(page)
await human_delay(1.5, 3.0)
# Then navigate to your actual target
⚠️ Pitfall: Too much randomness is also suspicious. Delays of 0.01–30 seconds look like a broken bot. Keep ranges realistic: 1–4 seconds for page reads, 0.3–1.5 seconds for scroll chunks.
9. The DataDome CAPTCHA: When Detection Fails Gracefully
Even with all five layers addressed, high-traffic targets will periodically challenge you. DataDome’s CAPTCHA system is predominantly a slider challenge — not a checkbox or image grid. It measures the physics of how you drag the slider.
Your options when a CAPTCHA appears:
Option A — CAPTCHA Solving Services (e.g. 2Captcha, CapSolver):
from twocaptcha import TwoCaptcha
solver = TwoCaptcha("YOUR_API_KEY")
result = solver.datadome(
pageurl="https://target-site.com/blocked-page",
captcha_url="https://geo.captcha-delivery.com/captcha/..."
)
token = result["code"]
# Inject the token back into the session cookie
Option B — Prevention over cure: clean IPs + realistic behavior + proper fingerprints = fewer CAPTCHAs appearing in the first place. Solvers cost $1–3 per 1,000 CAPTCHAs and add 5–20 seconds each. At scale, prevention has a far better ROI.
10. The Full Architecture: Mermaid Diagram
Here’s how all five layers connect in a production-grade DataDome bypass pipeline:

11. Putting It Together: A Complete DataDome Bypass Class
import asyncio
import random
import time
from curl_cffi import requests as cffi_requests
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async
PROXIES = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
HEADERS = {
"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"
),
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
}
class DataDomeBypass:
"""
Layered DataDome bypass combining:
- TLS impersonation (curl_cffi) → Layer 1
- Geo-coherent headers → Layer 2
- Residential proxy rotation → Layer 3
- Stealth browser (Playwright) → Layer 4
- Behavioral warm-up + human delays → Layer 5
"""
def _get_proxy(self) -> dict:
p = random.choice(PROXIES)
return {"http": p, "https": p}
def fetch_static(self, url: str, retries: int = 3) -> str | None:
"""For non-JS pages: fast, low-resource curl_cffi path."""
for attempt in range(retries):
try:
session = cffi_requests.Session(impersonate="chrome")
resp = session.get(
url,
headers=HEADERS,
proxies=self._get_proxy(),
timeout=15
)
if "datadome" in resp.cookies:
print(f"[Attempt {attempt+1}] DataDome cookie set - rotating...")
time.sleep(random.uniform(5, 10))
continue
if resp.status_code == 200:
return resp.text
except Exception as e:
print(f"[Static] Error: {e}")
time.sleep(random.uniform(2, 5))
return None
async def fetch_dynamic(self, url: str, base_url: str) -> str | None:
"""For JS-rendered pages: stealth Playwright with warm-up."""
proxy = random.choice(PROXIES)
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
proxy={"server": proxy},
args=["--disable-blink-features=AutomationControlled"]
)
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=HEADERS["User-Agent"],
locale="en-US",
timezone_id="America/New_York",
)
page = await context.new_page()
await stealth_async(page)
# Warm up: homepage first, then target
await page.goto(base_url, wait_until="domcontentloaded")
await asyncio.sleep(random.uniform(2.0, 3.5))
# Natural scroll on homepage
for _ in range(random.randint(2, 4)):
await page.mouse.wheel(0, random.randint(100, 300))
await asyncio.sleep(random.uniform(0.5, 1.2))
# Now go to the actual target
await page.goto(url, wait_until="networkidle")
await asyncio.sleep(random.uniform(1.5, 3.0))
content = await page.content()
await browser.close()
if "datadome" in content.lower() and "captcha" in content.lower():
print("[Dynamic] CAPTCHA encountered - solver needed.")
return None
return content
12. The Maintenance Problem Nobody Mentions
Here’s what every DataDome bypass tutorial conveniently skips: this breaks regularly.
DataDome updates its detection models continuously. A fingerprint that worked last Tuesday may trigger blocks next Monday. The playwright-stealth library is an open-source community project — it doesn't have a team of engineers tracking DataDome's weekly releases. Neither does undetected-chromedriver.
Maintaining a production DataDome bypass stack requires (Scrapfly, 2026):
- Monitoring fingerprint changes with every Chrome/Firefox major release
- Replacing flagged proxy IPs on a rolling basis
- Updating behavioral patterns as DataDome’s ML models improve
- Regression-testing your entire pipeline after every library update
For small-scale or one-off scraping, this is manageable. For production pipelines running at scale, this maintenance cost adds up fast.
13. When to Stop Reinventing the Wheel
At some point, the build-vs-buy math flips. If your team is spending more time maintaining evasion logic than extracting value from the data, something is wrong.
Tools like Bright Data’s Web Unlocker handle all five detection layers under a single API call — TLS impersonation, residential proxy rotation, browser fingerprinting, behavioral simulation, and CAPTCHA solving — without you maintaining any of it. Their Scraping Browser exposes a Playwright-compatible CDP endpoint that already passes DataDome’s checks out of the box.
(Full disclosure: I’m not affiliated with Bright Data. I’ve just seen it come up consistently in the research community as one of the few tools that actually handles DataDome-specific endpoints, including mobile API paths.)
The math that triggers the switch:
- You’re hitting DataDome-protected sites at thousands of requests per day
- Your target rotates its anti-bot vendor without warning
- Your team’s time is more valuable than the proxy/solver API cost
- You need 99%+ success rates for downstream data pipelines
Managed infrastructure isn’t a cop-out. It’s the right tool when DIY maintenance cost exceeds the subscription cost.
14. Legal and Ethical Considerations
Before deploying any of the above against a live target, run through this checklist:
✅ Check robots.txt — it's an ethical signal and legal reference in many jurisdictions (hiQ Labs v. LinkedIn, 9th Circuit 2022)
✅ Review Terms of Service — DataDome-protected sites often have explicit no-scraping clauses
✅ Avoid personal data — GDPR and CCPA apply to scraped EU/CA resident information
✅ Rate-limit responsibly — hammering a server can constitute a CFAA violation in the US
✅ Prefer official APIs — faster, more stable, and legally unambiguous
Scraping publicly available data for research, price monitoring, and competitive intelligence is generally considered lawful in the US and EU, as long as you’re not bypassing authentication walls or harvesting private user data. When in doubt, consult a lawyer familiar with digital rights law — the landscape continues to evolve.
Summary: The DataDome Bypass Checklist


