Cloudflare Turnstile in Playwright: Why Your Tests Stall and How to Solve It in 8 Lines
Cloudflare Turnstile in Playwright: Why Your Tests Stall and How to Solve It in 8 Lines If you're running Playwright or Selenium against any site behind Cloudflare, you've already met Turnstile. It's the new "managed challenge" widget Cloudflare started shipping in 2023, and it now appears in front of login flows, contact forms, signup pages, and increasingly the entire site root. Here's the part most teams miss: Turnstile doesn't always show a checkbox. A lot of the time it just sits invisible, runs its scoring loop, and either issues a token silently or stalls forever. Your test doesn't crash. It just times out at the next page.click("button[type=submit]") . The CI log says "element not interactable." Nobody knows why. I work on CaptchaAI. I'm going to show you exactly what's happening, then drop in 8 lines that fix it. The real scenario You have a Playwright suite that runs every PR. One day a test starts failing on the signup flow. You re-run it. It fails again. Locally on your laptop it passes. On CI it doesn't. What's actually happening: Cloudflare flagged your CI runner's IP block (GitHub Actions, GitLab runners, Hetzner, OVH, DO — all of them are on Cloudflare's "elevated risk" list). Turnstile decides to switch from invisible mode to "managed challenge" mode. Now there's a widget in the DOM that needs a real token before the form submit will accept. Your test never interacted with the widget because last week it didn't exist. Why retries don't help The instinct is to add a retry: 2 and move on. Don't. Cloudflare's scoring is per-IP-per-fingerprint, and each retry from the same runner makes the next challenge harder, not easier. After ~3 attempts you'll get full block pages instead of the widget. The right move is to solve the widget once, inject the token, and submit normally — exactly what a human user does, just faster. How Turnstile actually issues a token The widget renders an iframe pointing at challenges.cloudflare.com . Inside the iframe it runs a fi