How Cloudflare Turnstile Spots Bots: A Look at CAPTCHA-less Verification
TL;DR
Cloudflare Turnstile detects bots without requiring any user interaction, as a replacement for CAPTCHA. It runs four background challenges (Proof-of-Work, Proof-of-Space, Web API probes, behavioral analysis), combines them via machine learning, and issues a token. Automated control via CDP (Chrome DevTools Protocol) is blocked instantly via environment fingerprinting, and recovering within the same session is difficult (though officially, retry and reset mechanisms do exist).
The Trigger: Hitting a Wall on Form Auto-Fill
I was auto-filling a web form via Chrome DevTools MCP and the Submit button stayed disabled forever. When I forcibly ran btn.disabled = false in JS and clicked, I got “Cloudflare Turnstile verification failed.” Reload, manual click — nothing worked, and I couldn’t submit again from that browser session ever.
Why are automation tools detected? How does Turnstile work in the first place? When I dug in, the defense turned out to be more layered than I’d imagined.
What Was Wrong With CAPTCHA?
Traditional CAPTCHA had humans prove themselves by solving visual puzzles like “read these distorted letters” or “select the traffic lights.” The problems are clear.
- Terrible UX: the stress of having to solve a puzzle every time
- Accessibility wall: effectively excludes the visually impaired
- Solvable by bots: with advances in machine learning, image-recognition CAPTCHA solve rates keep going up
- Increased drop-off: it’s been reported that the mere presence of CAPTCHA can increase form abandonment by up to 40%
Cloudflare’s answer was “make the user do nothing in the first place.”
Turnstile’s Four-Layer Challenges
Turnstile runs invisible challenges simultaneously, in parallel. The difficulty automatically adjusts to the visitor’s risk level.
Note: The challenge composition below is based on technical analysis from reverse-engineering work; Cloudflare doesn’t publicly disclose internal implementation details. Implementations can change without notice (information as of February 2026).
flowchart LR
V["Visitor opens page"] --> W["Turnstile JS<br/>widget loads"]
W --> C1["Proof-of-Work"]
W --> C2["Proof-of-Space"]
W --> C3["Web API probe"]
W --> C4["Behavioral analysis"]
C1 --> ML["ML model<br/>risk judgment"]
C2 --> ML
C3 --> ML
C4 --> ML
ML -->|low risk| T["Issue token"]
ML -->|high risk| B["Block / further verification"]
1. Proof-of-Work (computational proof)
Have the browser solve a small computational task. The point isn’t to measure CPU load — it’s to impose a computational cost.
The principle is the same as Bitcoin mining: pose a problem like “find a nonce such that the leading N bits of SHA-256(nonce + challenge) are zero.” The browser has to brute-force the nonce (solving is O(2^N)), but server-side verification only needs one hash (O(1)). This asymmetry is the heart of the design.
For legitimate users, the difficulty N is set low enough that it finishes in milliseconds to a few hundred milliseconds. But if a bot farm runs 10,000 sessions in parallel, it needs 10,000 PoW computations, and CPU cost piles up linearly. It’s an economic deterrent: “cheap on a single machine, doesn’t pay off at scale.” Secondarily, an unusually fast solve time can serve as a clue that “this is being solved on something other than a browser, on high-performance hardware.”
2. Proof-of-Space (memory proof)
Where Proof-of-Work imposes “CPU time” on bots, Proof-of-Space imposes “memory.” Cloudflare disclosed the existence of this challenge type when announcing Turnstile but hasn’t disclosed specific algorithms or memory sizes.
What’s known is that the browser is made to allocate memory and perform a computation tied to that allocation. An amount of memory that’s no problem for a regular browser becomes problematic for a bot farm running thousands of sessions in parallel on a single server, since each session needs memory — putting a physical cap on parallelism.
About terminology: Academically, “Proof-of-Space” usually refers to disk storage proofs (Ateniese et al., Dziembowski et al.). What Cloudflare calls “Proof-of-Space” in Turnstile is a RAM allocation challenge, distinct from the cryptography literature context.
3. Web API Probes
This is the cleverest part. Whether the browser is real is judged by the behavior of the browser’s APIs.
Concretely, signals like the following are observed.
- Canvas/WebGL fingerprinting: outputs of identical drawing commands differ subtly per browser/GPU/driver combination
- Consistency of the navigator object: is
navigator.webdriverset totrue? Isnavigator.pluginsan empty array? - Performance API timings: do page-load timing patterns match human operation?
- CDP detection: traces of DevTools Protocol commands like
Runtime.enableorPage.enablehaving been executed
The last item is what tripped me up. Operating a page via the Chrome DevTools Protocol leaves detectable traces in the browser’s runtime. Behavioral changes in window.chrome.csi, format differences in Error.stack, binding objects DevTools injects — multiple signals are used to detect CDP control.
4. Behavioral Analysis
Mouse movements, scroll patterns, key-press timings, and page-transition patterns are analyzed comprehensively. Human operation has its own distinctive “noise.” Nobody clicks at perfectly even intervals, and no mouse trajectory is a perfectly straight line.
The key here is that judgment isn’t made on a single signal. All challenge results are fed into an ML model, and risk is judged on the composite score.
Token Issuance and Verification Flow
When a challenge passes, Turnstile issues a JWT-like verification token. This token is sent to the server when the form is submitted.
sequenceDiagram
participant B as Browser
participant T as Turnstile JS
participant CF as Cloudflare
participant S as Site server
B->>T: Page load (init with Sitekey)
T->>CF: Fetch challenge
CF-->>T: Deliver challenge
T->>T: Run 4-layer challenges
T->>CF: Send challenge results
CF-->>T: Issue verification token
Note over T: Set token in hidden input
B->>S: Submit form (with token)
S->>CF: Verify token via Siteverify API
CF-->>S: Verification result (success/failure)
S-->>B: Response
Server-side verification is done by a POST request to https://challenges.cloudflare.com/turnstile/v0/siteverify. Send the Secret Key and the token, and it verifies validity, issuance time, and host-name match.
Tokens have an expiration. With the Pre-Clearance feature you can reuse via cookies within a session, but basically tokens are one-shot.
The Three Widget Modes
Turnstile provides three modes for different use cases.
| Mode | Visible to user? | Interaction | Use case |
|---|---|---|---|
| Managed | Shown according to risk | Checkbox only when needed | General (recommended) |
| Non-interactive | Invisible | None | UX-first |
| Invisible | Invisible | None | Fully hidden |
Managed is the most flexible — Cloudflare automatically judges risk and only shows a checkbox when something looks suspicious. The vast majority of users pass through without seeing anything.
Why Can’t a CDP-Controlled Browser Recover?
What was most interesting this time was this behavior: in my environment, once a browser session was connected via CDP, neither reload nor switching to manual operation got it past Turnstile’s challenge.
Note: The following is based on observations from my macOS + Chrome + Chrome DevTools MCP environment. Behavior may differ depending on browser version, OS, or Turnstile-side updates. Cloudflare’s official docs describe automatic challenge retry and the
turnstile.reset()reset mechanism, so a recovery path does exist in theory.
Plausible reasons:
- Detection of browser launch flags: starting Chrome with flags like
--remote-debugging-portintroduces detectable changes into the browser’s runtime environment - Session-level trust score: Turnstile likely manages trust per session rather than per page. Sessions judged as “automation environments” tend to keep a low trust score
- Preconditions for the Turnstile token: token issuance presupposes “challenge results from a trustworthy environment.” If the environment’s trust score is low, even correct challenge results don’t easily produce a token
stateDiagram-v2
[*] --> Clean: Normal browser launch
[*] --> Tainted: Launched with CDP
Clean --> Verified: Pass challenge
Verified --> TokenIssued: Token issued
Tainted --> Blocked: Challenge rejected
Blocked --> Blocked: Reload/manual operation (no recovery in my environment)
Blocked --> Retry: turnstile.reset() (officially retryable)
Retry --> Blocked: Re-fails if environment signals don't change
In my environment, the strategy “fill in automatically and then click Submit manually” didn’t work. That doesn’t mean Turnstile categorically forbids recovery — rather, the environment signals under CDP control kept causing the challenge to fail.
Turnstile From the Implementer’s Side
Let’s also look at it from the perspective of integrating Turnstile into a website. The implementation is surprisingly simple.
Client side is one script tag and one div element.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAA..."></div>
Server side is just token verification.
import requests
def verify_turnstile(token: str, secret_key: str) -> bool:
resp = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": secret_key, "response": token}
)
return resp.json().get("success", False)
For SPAs, call turnstile.render() explicitly.
const widgetId = turnstile.render('#turnstile-container', {
sitekey: '0x4AAAAAAA...',
callback: (token) => {
// Include the token in form data on submit
document.getElementById('cf-token').value = token;
},
});
That’s all it takes for the four-layer challenges to run behind the scenes. It works without going through Cloudflare’s network, so you can keep your existing infrastructure as-is.
Comparison with CAPTCHA
| Aspect | Traditional CAPTCHA | Turnstile |
|---|---|---|
| User action | Solve a puzzle | None (in most cases) |
| Judgment method | Puzzle correctness | Composite environment + behavior score |
| Bot resistance | Bypassable via image recognition | High resistance from layered signals |
| Accessibility | Difficult for visually impaired | No problem since no operation is needed |
| Privacy | Often used for ad tracking | Doesn’t collect tracking data |
| Impact on drop-off | Large | Minimal |
Conclusion
Turnstile’s design philosophy is clear: shift “proving you’re human” from being a user burden to a system responsibility.
The multi-layer defense combining four challenges (Proof-of-Work, Proof-of-Space, Web API probes, behavioral analysis) doesn’t depend on a single signal, so it’s robust. And the design where a session once judged as an “untrustworthy environment” doesn’t recover structurally prevents incremental breakthroughs by automation tools.
Personally, what struck me most was the stickiness of CDP detection. The fact that “automate the input, then submit manually” didn’t work (at least in my environment) is because the boundary between bot and human is drawn at “environment” rather than “operation.” Although retry and reset mechanisms exist officially, as long as the environment signal itself is tainted, recovery was effectively difficult.
By the way, I ended up just opening the form in normal Safari and copy-pasting from a text file. A good chance to feel the limits of automation firsthand.
That’s all.
References
- Cloudflare Turnstile Official Documentation — Challenge types, widget modes, Pre-Clearance, and Siteverify API specs
- Cloudflare Turnstile: What is that and how to solve it — Technical analysis of Turnstile’s internals, CDP detection mechanisms, and fingerprinting techniques
- Fighting API Bots with Cloudflare’s Invisible Turnstile — Troy Hunt’s case study on implementing Turnstile’s non-interactive challenge
- Defeat Cloudflare Turnstile — How Turnstile’s risk evaluation and challenge difficulty adjustment work