Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 4 min read

DataDome in Playwright: why rotating your proxy makes blocking worse

You add a proxy pool. You rotate it every request. The blocks get more frequent, not less. Then you read that DataDome needs "better residential IPs", you buy a bigger pool, and the block rate climbs again. I have spent

You add a proxy pool. You rotate it every request. The blocks get more frequent, not less. Then you read that DataDome needs "better residential IPs", you buy a bigger pool, and the block rate climbs again.

I have spent a lot of time on this failure mode. The short version: rotation is not a mitigation for DataDome, it is an accelerator. Here is the mechanism, and the two approaches that actually hold up.

What DataDome is actually scoring

A CAPTCHA from DataDome is the visible output of a score computed before it, from signals that mostly have nothing to do with your IP:

  • TLS fingerprint (JA3/JA4) and HTTP/2 fingerprint โ€” cipher order, extensions, SETTINGS frame, header order. Your proxy changes none of these. A real Chrome does.
  • Navigator and environment consistency โ€” userAgent, platform, hardwareConcurrency, deviceMemory, WebGL renderer string, canvas hash, fonts, audio context.
  • Coherence between identity and network โ€” a Europe/Paris timezone and fr-FR locale on an IP geolocated to a datacenter in Virginia is a signal on its own.
  • Behaviour before the challenge โ€” mouse movement, scroll, keystroke timing, request cadence.

Now consider a pool of 500 IPs all driven by one headless browser build. From DataDome's side, that is not 500 users. It is one fingerprint wearing 500 hats, and it gets scored accordingly. A stable IP with a consistent, plausible fingerprint is usually safer than a fresh IP every request.

The cookie is not portable

When DataDome does let you through, it drops a datadome cookie. That cookie is bound to the identity that earned it โ€” the IP it was issued to, the User-Agent sent with it, the TLS fingerprint of the client that used it.

So:

Symptom What it usually means
Cookie works, then dies when you rotate Cookie is bound to the original egress IP
Cookie dies only after you restart the browser Bound to the fingerprint / session, not just IP
Every request returns the interstitial, instantly Score already high from fingerprint reuse across many IPs
Works in curl, blocked in Playwright Header order, HTTP/2 fingerprint, or missing browser-only headers
Works locally, blocked on the server Timezone/locale vs IP mismatch, or a datacenter ASN

If you fix only the IP, you fix the symptom in row 1 and make rows 3 and 5 worse.

Approach 1 โ€” one stable identity per worker

Instead of rotating, give each worker its own long-lived identity and never rotate mid-session:

  • One persistent browser context per worker (launch_persistent_context), so cookies and storage survive between pages.
  • One sticky proxy per worker for the life of that context โ€” not per request.
  • Warm the session on a benign page first so there is real navigation history and interaction telemetry before you hit the protected URL.
  • Keep the browser build patched and consistent with the UA string you report.

This is slower to set up and uses more memory, which is exactly why rotation is popular โ€” but it is the only shape that survives a persistent score.

Approach 2 โ€” solve the interstitial out of band, reuse the cookie

If you do not need a full browser on every request, you can let a solver earn the cookie once and then replay it from your normal HTTP client. You submit the protected URL and the challenge URL, and get back the cookie plus the User-Agent that was used to obtain it.

from solvehub import SolveHub

client = SolveHub("sh_live_xxx")

task = client.solve(
    "datadome",
    {
        "websiteURL": "https://example.com",
        "captchaUrl": "https://example.com/captcha",
    },
    proxy="http://user:pass@host:port",   # the egress IP the cookie will be bound to
)

cookies    = task["solution"]["cookies"]      # {"datadome": "..."}
user_agent = task["solution"]["userAgent"]

Then the part everyone gets wrong:

session = requests.Session()
session.headers["User-Agent"] = user_agent        # must match, verbatim
session.cookies.update(cookies)                   # same cookie domain
r = session.get("https://example.com/target")     # from the SAME egress IP

Three rules that decide whether this works:

  1. Same egress IP. The cookie was issued to one IP. Sending it from another is what makes people conclude "the solver does not work".
  2. Byte-identical User-Agent. Not the same family โ€” the same string, including the Chrome build suffix.
  3. Re-solve when it expires. These cookies are short-lived by design. Cache per identity with a TTL, not forever.

Which one to choose

  • Full browser automation with JS-heavy pages and interactive flows: Approach 1.
  • High-volume read paths, or when Playwright is too expensive per request: Approach 2.
  • Both together: persist the context for navigation, refresh the cookie out of band when the score degrades.

The one change that helps in every case is to stop treating IP rotation as the answer. Fix the fingerprint, keep identities stable, and rotate identities โ€” not IPs on a single identity.

Disclosure: I build SolveHub, a CAPTCHA API that handles DataDome, Cloudflare Turnstile and Interstitial, Akamai, PerimeterX, Imperva, AWS WAF, Arkose FunCaptcha and image OCR through a single endpoint. Use it only against systems you own or are authorized to test.

Related in this series

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.