<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lsr</title>
    <description>The latest articles on DEV Community by Lsr (@seencxy).</description>
    <link>https://dev.to/seencxy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4233232%2Fd72422a4-5c3c-4932-87ba-8d1392d989cc.png</url>
      <title>DEV Community: Lsr</title>
      <link>https://dev.to/seencxy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seencxy"/>
    <language>en</language>
    <item>
      <title>Eight checks before you buy a CAPTCHA solver: the 403 debugging ladder</title>
      <dc:creator>Lsr</dc:creator>
      <pubDate>Sun, 08 Nov 2026 07:20:52 +0000</pubDate>
      <link>https://dev.to/seencxy/eight-checks-before-you-buy-a-captcha-solver-the-403-debugging-ladder-2d2j</link>
      <guid>https://dev.to/seencxy/eight-checks-before-you-buy-a-captcha-solver-the-403-debugging-ladder-2d2j</guid>
      <description>&lt;p&gt;You have a 403. The internet has two answers ready: buy a proxy, or buy a CAPTCHA solver. Both are right maybe half the time, and the wrong one costs you a month of engineering before you notice.&lt;/p&gt;

&lt;p&gt;This is the ladder I run before spending money on either. Each step is cheaper than the one below it, and most 403s stop at step 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Read the response, do not guess what blocked you
&lt;/h2&gt;

&lt;p&gt;A 403 from a WAF and a 403 from an application login check look identical in your exception handler and completely different in the headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s1"&gt;'https://target.example/page'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cf-ray&lt;/code&gt; plus &lt;code&gt;cf-mitigated: challenge&lt;/code&gt; — Cloudflare is challenging you, not the app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-datadome&lt;/code&gt; or a &lt;code&gt;datadome&lt;/code&gt; cookie — DataDome.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_abck&lt;/code&gt; or &lt;code&gt;bm_sz&lt;/code&gt; — Akamai Bot Manager.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_px3&lt;/code&gt; or &lt;code&gt;_pxhd&lt;/code&gt; — PerimeterX / HUMAN.&lt;/li&gt;
&lt;li&gt;A 200 whose body is 2 KB of JavaScript and no content — that is a challenge page, not a success.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single distinction decides the next seven steps. If the body is the real page and the app says no, no amount of solving will help you.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Is it the environment or the IP?
&lt;/h2&gt;

&lt;p&gt;Run the same request from a different network — your laptop on home broadband is enough.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works from home, fails from the datacenter: it is IP reputation, and a proxy is the right purchase.&lt;/li&gt;
&lt;li&gt;Fails from both: it is you (fingerprint, headers, cookies) or it is the account. A proxy will only make it worse and more expensive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Real browser or headless?
&lt;/h2&gt;

&lt;p&gt;If a normal browser loads the page and Playwright does not, the difference is TLS/JA4, HTTP/2 settings or header order — not your IP, and not something a solver fixes. Compare your client fingerprint against the browser with any JA3/JA4 checker before spending on either.&lt;/p&gt;

&lt;p&gt;Two reliable fixes here: run a real browser build with matching versions, or use a client that reproduces the browser fingerprint. Rotating proxies at this stage is the classic mistake — five hundred IPs behind one headless fingerprint is one user with five hundred hats.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. If a token is required, which token?
&lt;/h2&gt;

&lt;p&gt;Solver APIs are not interchangeable. A Turnstile token is useless on an Akamai page, and a &lt;code&gt;datadome&lt;/code&gt; cookie is not a token at all — it is a session credential bound to the IP and User-Agent that received it. Identify the challenge from step 1, then ask for that specific thing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;How you recognise it&lt;/th&gt;
&lt;th&gt;Price per 1,000 solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Turnstile&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cf-turnstile&lt;/code&gt; widget, &lt;code&gt;0x4AAA...&lt;/code&gt; sitekey&lt;/td&gt;
&lt;td&gt;from $0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Interstitial&lt;/td&gt;
&lt;td&gt;full-page check, no widget to click&lt;/td&gt;
&lt;td&gt;$0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Akamai Bot Manager&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_abck&lt;/code&gt;, &lt;code&gt;bm_sz&lt;/code&gt; cookies&lt;/td&gt;
&lt;td&gt;$0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerimeterX / HUMAN&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_px3&lt;/code&gt; cookie, &lt;code&gt;px-captcha&lt;/code&gt; frame&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS WAF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aws-waf-token&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataDome&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;x-datadome&lt;/code&gt; header, &lt;code&gt;datadome&lt;/code&gt; cookie&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Imperva&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;incap_ses_&lt;/code&gt;, &lt;code&gt;visid_incap_&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;$1.60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arkose FunCaptcha&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;arkoselabs&lt;/code&gt; script, &lt;code&gt;funcaptcha&lt;/code&gt; frame&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prices above are from one vendor — mine, see the note at the bottom — and they are there to make the point that the spread is 10x. Paying FunCaptcha money for a Turnstile page is a silent 10x overrun.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Are you submitting the token correctly?
&lt;/h2&gt;

&lt;p&gt;This is the most common self-inflicted 403, and it looks exactly like a solver that does not work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sitekey and page must match.&lt;/strong&gt; A token minted for &lt;code&gt;example.com/login&lt;/code&gt; will not validate on &lt;code&gt;example.com/checkout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;action&lt;/code&gt; and &lt;code&gt;cdata&lt;/code&gt; must be carried over.&lt;/strong&gt; If the page passes them, the token has to be minted with the same values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tokens are single-use and short-lived.&lt;/strong&gt; Turnstile tokens are valid for about 300 seconds and one submission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post the field name the page expects.&lt;/strong&gt; &lt;code&gt;cf-turnstile-response&lt;/code&gt;, &lt;code&gt;g-recaptcha-response&lt;/code&gt;, &lt;code&gt;h-captcha-response&lt;/code&gt; — three different names, one wrong guess sends you back to the challenge.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. For cookie challenges, identity must not move
&lt;/h2&gt;

&lt;p&gt;DataDome, Akamai, PerimeterX, Imperva and AWS WAF hand you a cookie that is bound to the IP and User-Agent that earned it. Reusing it from a different proxy exit fails instantly, and the failure looks like the solver produced a bad value.&lt;/p&gt;

&lt;p&gt;Rule: one identity per session — same proxy, same User-Agent, same cookie jar. Rotate identities between sessions, never inside one.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Check your own traffic shape before blaming the target
&lt;/h2&gt;

&lt;p&gt;A scraper that opens 40 parallel requests against one host gets challenged regardless of how clean the requests are. Concurrency of 4 to 8 per host, a small delay, and reusing a session for a whole crawl will outperform any fingerprint trick. If your logs show a challenge on the first request every time, this is not a capacity problem — go back to step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Do the unit economics on accepted answers, not attempts
&lt;/h2&gt;

&lt;p&gt;This is the step almost nobody does, and it changes which vendor wins. If you pay per solved task, failed solves are free, so your real cost is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost per accepted answer = price per solved task / acceptance rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A $0.20 solve that the site rejects adds a retry, a new session and possibly a raised risk score. Two retries at $0.20 plus a session reset can cost more than one $1.20 solve that lands first time. Price per 1,000 is a starting point; cost per accepted answer is the number that shows up in your invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a solver actually fits
&lt;/h2&gt;

&lt;p&gt;If steps 1 to 7 leave you with a page that is blocked purely because it wants a token or cookie you cannot produce headlessly, that is the moment to buy solving capacity — and to buy it by challenge type, not by volume. I run &lt;a href="https://tiam.codes" rel="noopener noreferrer"&gt;SolveHub&lt;/a&gt;, one REST endpoint for the eight challenge types in the table above, billed only on completed tasks, with the cookie challenges accepting your own proxy so the credential comes back usable.&lt;/p&gt;

&lt;p&gt;Client code, if you want it: &lt;a href="https://github.com/seencxy/solvehub-python" rel="noopener noreferrer"&gt;solvehub-python&lt;/a&gt; (stdlib only) and &lt;a href="https://github.com/seencxy/solvehub-node" rel="noopener noreferrer"&gt;solvehub-node&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The two earlier posts in this series go deeper on the two most common cases: &lt;a href="https://dev.to/seencxy/cloudflare-turnstile-in-a-python-scraper-tokens-expiry-and-three-approaches-5592"&gt;Turnstile tokens and expiry&lt;/a&gt; and &lt;a href="https://dev.to/seencxy/datadome-in-playwright-why-rotating-your-proxy-makes-blocking-worse-2259"&gt;why rotating proxies makes DataDome worse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: I build the solver API linked above. Everything in steps 1 to 7 applies whether you buy from me or from nobody.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Use only on systems you own or are authorised to test.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>DataDome in Playwright: why rotating your proxy makes blocking worse</title>
      <dc:creator>Lsr</dc:creator>
      <pubDate>Sun, 08 Nov 2026 07:10:18 +0000</pubDate>
      <link>https://dev.to/seencxy/datadome-in-playwright-why-rotating-your-proxy-makes-blocking-worse-2259</link>
      <guid>https://dev.to/seencxy/datadome-in-playwright-why-rotating-your-proxy-makes-blocking-worse-2259</guid>
      <description>&lt;p&gt;You add a proxy pool. You rotate it every request. The blocks get &lt;em&gt;more&lt;/em&gt; frequent, not less. Then you read that DataDome needs "better residential IPs", you buy a bigger pool, and the block rate climbs again.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What DataDome is actually scoring
&lt;/h2&gt;

&lt;p&gt;A CAPTCHA from DataDome is the visible output of a score computed &lt;em&gt;before&lt;/em&gt; it, from signals that mostly have nothing to do with your IP:&lt;/p&gt;

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

&lt;p&gt;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 &lt;strong&gt;one fingerprint wearing 500 hats&lt;/strong&gt;, and it gets scored accordingly. A stable IP with a consistent, plausible fingerprint is usually &lt;em&gt;safer&lt;/em&gt; than a fresh IP every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cookie is not portable
&lt;/h2&gt;

&lt;p&gt;When DataDome does let you through, it drops a &lt;code&gt;datadome&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

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

&lt;p&gt;If you fix only the IP, you fix the symptom in row 1 and make rows 3 and 5 worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1 — one stable identity per worker
&lt;/h2&gt;

&lt;p&gt;Instead of rotating, give each worker &lt;strong&gt;its own long-lived identity&lt;/strong&gt; and never rotate mid-session:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2 — solve the interstitial out of band, reuse the cookie
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;solvehub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SolveHub&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SolveHub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh_live_xxx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datadome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;websiteURL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captchaUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/captcha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user:pass@host:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# the egress IP the cookie will be bound to
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cookies&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# {"datadome": "..."}
&lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the part everyone gets wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_agent&lt;/span&gt;        &lt;span class="c1"&gt;# must match, verbatim
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;# same cookie domain
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# from the SAME egress IP
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules that decide whether this works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Same egress IP.&lt;/strong&gt; The cookie was issued to one IP. Sending it from another is what makes people conclude "the solver does not work".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte-identical User-Agent.&lt;/strong&gt; Not the same &lt;em&gt;family&lt;/em&gt; — the same string, including the Chrome build suffix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-solve when it expires.&lt;/strong&gt; These cookies are short-lived by design. Cache per identity with a TTL, not forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which one to choose
&lt;/h2&gt;

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

&lt;p&gt;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 &lt;em&gt;identities&lt;/em&gt; — not IPs on a single identity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://www.tiam.codes" rel="noopener noreferrer"&gt;SolveHub&lt;/a&gt;, 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.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Related in this series
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Turnstile tokens and expiry in Python: &lt;a href="https://dev.to/seencxy/cloudflare-turnstile-in-a-python-scraper-tokens-expiry-and-three-approaches-5592"&gt;seencxy/cloudflare-turnstile-in-a-python-scraper-tokens-expiry-and-three-approaches-5592&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Runnable client code: &lt;a href="https://github.com/seencxy/solvehub-python" rel="noopener noreferrer"&gt;solvehub-python&lt;/a&gt; · &lt;a href="https://github.com/seencxy/solvehub-node" rel="noopener noreferrer"&gt;solvehub-node&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cloudflare Turnstile in a Python scraper: tokens, expiry, and three approaches</title>
      <dc:creator>Lsr</dc:creator>
      <pubDate>Sun, 08 Nov 2026 07:03:56 +0000</pubDate>
      <link>https://dev.to/seencxy/cloudflare-turnstile-in-a-python-scraper-tokens-expiry-and-three-approaches-5592</link>
      <guid>https://dev.to/seencxy/cloudflare-turnstile-in-a-python-scraper-tokens-expiry-and-three-approaches-5592</guid>
      <description>&lt;p&gt;If you scrape a site that sits behind Cloudflare Turnstile, you have probably seen the same loop: the page loads, a small widget renders, and every request without a valid token comes back as a 403 with a challenge page.&lt;/p&gt;

&lt;p&gt;This post is about the mechanics of that token — where it comes from, how long it lives, why a "working" solver still gives you 403s — and the three approaches that actually work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Turnstile actually gives you
&lt;/h2&gt;

&lt;p&gt;Turnstile replaces the old image CAPTCHA with an invisible check that combines proof-of-work with a JavaScript fingerprint. When it succeeds, the widget hands your page a token: a long opaque string. You submit that token with your form (usually in a hidden field, sometimes in a header), and Cloudflare's edge validates it.&lt;/p&gt;

&lt;p&gt;Three properties matter more than anything else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The token is bound to the sitekey and the hostname.&lt;/strong&gt; A token minted for &lt;code&gt;example.com&lt;/code&gt; will not validate on &lt;code&gt;www.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It expires in 300 seconds.&lt;/strong&gt; Turnstile tokens live for five minutes, and each one is single-use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional fields are part of the signature.&lt;/strong&gt; If the page passes &lt;code&gt;action&lt;/code&gt; or &lt;code&gt;cdata&lt;/code&gt; to &lt;code&gt;turnstile.render&lt;/code&gt;, your solution has to carry the same values or validation fails with no useful error message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most "the solver doesn't work" reports are one of those three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 1: drive a real browser
&lt;/h2&gt;

&lt;p&gt;At low volume, Playwright (or Patchright, if you want the patched CDP layer) with a persistent profile is usually enough — the widget runs its check in a real JS environment and you simply read the token out of the DOM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;headless&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--disable-blink-features=AutomationControlled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;storage_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;() =&amp;gt; document.querySelector(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[name=cf-turnstile-response]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)?.value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval_on_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[name=cf-turnstile-response]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;el =&amp;gt; el.value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The costs are real: a browser process per concurrency slot, 100–300 MB RSS each, and it breaks whenever the fingerprint check gets stricter. Fine for a handful of pages an hour, painful at thousands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approach 2: outsource the token
&lt;/h2&gt;

&lt;p&gt;The second option keeps your scraper on plain HTTP and lets a service produce the token. &lt;a href="https://tiam.codes" rel="noopener noreferrer"&gt;SolveHub&lt;/a&gt; (disclosure: I build it) is one; CapSolver, 2Captcha and YesCaptcha work the same way. The shape is identical everywhere: create a task with the sitekey and page URL, poll until it completes, read the token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;solvehub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SolveHub&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SolveHub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh_live_xxx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turnstile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;websiteURL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;websiteKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0x4AAAAAAA_SITE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# only if the page sets one
&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then post that token back to the page inside its five-minute window, from the same IP and User-Agent that fetched the page.&lt;/p&gt;

&lt;p&gt;Two things break this in practice. First, free proxies: a token solved on a residential IP and replayed from a datacenter IP gets rejected. Second, queue latency — if your job sits in a queue for six minutes, the token is already dead.&lt;/p&gt;

&lt;p&gt;For cookie-based challenges (DataDome, Akamai, PerimeterX, Imperva, AWS WAF) there is no token at all. You get cookies back, and those cookies are tied to the IP and User-Agent that solved them. Reuse all three together or the next request is challenged again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datadome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;websiteURL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captchaUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/captcha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user:pass@host:port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="c1"&gt;# {"datadome": "..."}
&lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;solution&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userAgent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Approach 3: don't fight the edge
&lt;/h2&gt;

&lt;p&gt;Worth saying out loud: some sites publish exactly the data you want through an official API, a feed or a sitemap. If a product has a documented endpoint, use it — it is faster, stable, and nobody has to reverse-engineer anything. Only reach for the previous two approaches when there is genuinely no supported path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A five-line checklist for 403s
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Usual cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;403 after submitting a token&lt;/td&gt;
&lt;td&gt;token expired or already used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403 with a fresh token&lt;/td&gt;
&lt;td&gt;hostname / sitekey mismatch, or &lt;code&gt;action&lt;/code&gt; + &lt;code&gt;cdata&lt;/code&gt; not carried over&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works locally, fails in production&lt;/td&gt;
&lt;td&gt;different egress IP than the one that solved it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Challenge on every single request&lt;/td&gt;
&lt;td&gt;cookies not persisted between requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works for a while, then stops&lt;/td&gt;
&lt;td&gt;fingerprint change, or your UA rotating mid-session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Legal note
&lt;/h2&gt;

&lt;p&gt;Everything above is for systems you own or have written permission to test. Solving a third party's anti-bot challenge can violate their terms of service, and Cloudflare itself treats automated solving as an attack signal. Get permission first, and put it in writing.&lt;/p&gt;




&lt;h3&gt;
  
  
  Related in this series
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why rotating proxies makes DataDome blocking worse: &lt;a href="https://dev.to/seencxy/datadome-in-playwright-why-rotating-your-proxy-makes-blocking-worse-2259"&gt;seencxy/datadome-in-playwright-why-rotating-your-proxy-makes-blocking-worse-2259&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Runnable client code: &lt;a href="https://github.com/seencxy/solvehub-python" rel="noopener noreferrer"&gt;solvehub-python&lt;/a&gt; · &lt;a href="https://github.com/seencxy/solvehub-node" rel="noopener noreferrer"&gt;solvehub-node&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>security</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
