Cloudflare Turnstile vs reCAPTCHA: why we migrated and what it cost us
We pulled reCAPTCHA out of three production apps and replaced it with Cloudflare Turnstile. Here's what broke, what it saved, and where Turnstile still falls short.
- ↳Turnstile killed our reCAPTCHA UX complaints overnight: no more traffic light puzzles on the signup form.
- ↳Migration took about 6 hours across three apps, mostly because the server-side verify endpoint and response shape are slightly different from Google's.
- ↳Turnstile is free and unmetered, but you give up reCAPTCHA Enterprise's risk score granularity, which matters if you run aggressive fraud rules.
- ↳The biggest hidden cost was rewriting our analytics dashboards that depended on reCAPTCHA score buckets.
- ↳If your app already sits behind Cloudflare, there is almost no reason to keep paying attention to reCAPTCHA.
Last March a user emailed me a screenshot of our signup page. Eight tiles of fire hydrants, the spinning reload, then four more rounds of buses. He gave up. I would have too.
That was the moment I committed to ripping reCAPTCHA out of every app we run.
This is the writeup of what that actually took. Three apps, two weekends, one billing surprise, and one thing I still miss about Google’s product.
What we were running before
We had reCAPTCHA v3 on Photo AI Studio and Interior AI Designs, and reCAPTCHA v2 (the checkbox) on a smaller internal tool. v3 returns a score from 0.0 to 1.0 and you decide where to cut. v2 is the “I’m not a robot” checkbox that sometimes escalates to the image puzzle.
Our cut on v3 was 0.5. Anything below got a soft challenge (email verification loop), anything below 0.3 got blocked outright. We tuned this over about four months in 2024 by watching false positives in Sentry and complaints in Crisp.
The complaints never went to zero. Mobile Safari users on residential VPNs got hammered. Anyone using a privacy-focused browser like Brave with strict mode on would just see the form silently refuse to submit, because the grecaptcha script failed to load and our error handling was, in hindsight, garbage.
Why Turnstile
Three reasons, in order:
- We were already paying Cloudflare for DNS, Workers, and R2 on all three apps. Adding Turnstile is zero extra dollars and zero extra vendors.
- Turnstile does not require users to solve puzzles. It runs a series of non-interactive browser checks (proof of work, behavioral signals, environment integrity) and issues a token. Most users see a brief spinner and a checkmark.
- No third-party tracking cookies. Cloudflare’s pitch is that Turnstile does not feed an ad graph, which matters for our EU users and matters to me personally.
The Cloudflare docs (https://developers.cloudflare.com/turnstile/) claim it is a drop-in replacement for reCAPTCHA. That is mostly true. Mostly.
What the actual code change looked like
Client side, swapping the script tag and the widget is almost identical:
<!-- before -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<div class="g-recaptcha" data-sitekey="YOUR_KEY"></div>
<!-- after -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_KEY"></div>
Both widgets stuff a token into a hidden input named cf-turnstile-response or g-recaptcha-response. You post that to your server with the form.
Server side is where the small but annoying differences live. Here is our verify helper after the migration, running on a Cloudflare Worker:
export async function verifyTurnstile(token: string, ip: string) {
const body = new FormData();
body.append('secret', env.TURNSTILE_SECRET);
body.append('response', token);
body.append('remoteip', ip);
const res = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{ method: 'POST', body }
);
const data = await res.json<{
success: boolean;
'error-codes': string[];
challenge_ts: string;
hostname: string;
action?: string;
cdata?: string;
}>();
return data.success;
}
Notice what is missing compared to reCAPTCHA v3: there is no score field. Turnstile gives you a boolean. Pass or fail. That’s it.
This is the single biggest thing to think about before you migrate.
The score problem
With reCAPTCHA v3 I had dashboards in Metabase showing score distributions per route. I could see that our /api/generate endpoint was averaging 0.7, our /api/signup was averaging 0.6, and our /api/checkout (already gated by Stripe) was averaging 0.9. When something weird happened, like a botnet trying to burn through free generations, I would see the average score for that route drop within minutes.
Turnstile does not give you that. You get pass/fail and a set of error codes if it fails. That is a real downgrade for fraud forensics.
For Photo AI Studio this didn’t matter because we already have RevenueCat and Stripe-level signals to catch payment abuse. For Zoltan AI, where most usage is free-tier, it mattered more. I ended up adding our own lightweight behavioral signals (mouse movement entropy, time-on-page) and storing them in a signup_signals table so I could still build dashboards. About four hours of work.
If you have a real anti-abuse team running reCAPTCHA Enterprise (https://cloud.google.com/recaptcha) with Account Defender and the full risk analysis API, Turnstile is not at parity. Be honest about that.
What it actually cost us
Time, mostly. Roughly:
- 1 hour: swap the script tag and div in three apps, plus the Astro components that wrapped them.
- 2 hours: update the verify endpoint in each app. We had a shared utility, so this was less painful than it could have been.
- 4 hours: rebuild the analytics layer I just described.
- 1 hour: update the privacy policy and remove Google as a sub-processor.
- ~30 minutes: production smoke tests across Chrome, Safari, Firefox, and Brave.
Call it 8 to 10 hours of one engineer’s time, spread over a Saturday and a Sunday.
Dollar cost: Turnstile is free with no documented limit. reCAPTCHA v3 standard is also free up to 10k assessments per month, then $8 per 1k. We were not hitting that limit. The win was not the bill.
What I actually like about Turnstile in production
Three things, after about nine months in production:
The widget is small and quiet. Most users on a clean network see it resolve in under 800ms. No puzzles, ever, in our traffic. Our support inbox went from one or two CAPTCHA complaints per week to zero.
It works behind Cloudflare’s bot fight mode without weird interactions. We tried this with reCAPTCHA and got into a state where Cloudflare’s interstitial would block the reCAPTCHA script load. With Turnstile that class of bug is impossible.
The error codes are useful and documented. timeout-or-duplicate is the one you will see most. It means the token was already used or expired (300 second TTL). Treat it as a soft failure and re-issue.
What still bothers me
Two things.
First, Turnstile is harder to test locally. There is a set of test site keys documented at https://developers.cloudflare.com/turnstile/troubleshooting/testing/ that always pass or always fail, which is fine, but the dev experience is not as smooth as reCAPTCHA’s localhost allowlist. We pin a test key in .env.local and move on.
Second, I genuinely don’t know how Turnstile will perform if Cloudflare ever has a control-plane incident. reCAPTCHA running on Google infra and your app running on Cloudflare gave you a small amount of failure-domain separation. Putting both on Cloudflare means a bad day at Cloudflare is a really bad day for you. We accepted this. You should think about it.
When I would not migrate
If you are on reCAPTCHA Enterprise and using the risk scores to drive a real fraud model, stay. If your stack has no relationship with Cloudflare and you would be adding a new vendor to remove an existing one, the math is less obvious. If you have compliance reasons to keep a Google sub-processor, that is its own conversation.
For everyone else running a Cloudflare-fronted SaaS with a signup form and a contact form and maybe a comment box, Turnstile is the better default in 2025. The puzzles were a tax on real users and they did not buy us much.
If you want help auditing your own bot-protection setup or moving something off reCAPTCHA, get in touch.
Common questions
▸Is Cloudflare Turnstile really free with no limits?
Yes, as of writing. Cloudflare's Turnstile page lists it as free and unmetered with no published cap on verifications. You need a Cloudflare account but you do not need to be hosting your site on Cloudflare to use it. That said, free products can change. If your business depends on it, build your integration so you could swap providers in a day.
▸Does Turnstile work without Cloudflare hosting my site?
Yes. Turnstile is a standalone widget plus a siteverify endpoint at challenges.cloudflare.com. You can use it on a site hosted on Vercel, Netlify, Fly, Hetzner, anywhere. You only need a Cloudflare account to create the site key and secret. We use it both on Workers-hosted apps and on apps where Cloudflare is just doing DNS.
▸How do I replace a reCAPTCHA v3 score with Turnstile?
You can't, directly. Turnstile returns a boolean, not a 0.0-1.0 score. If you were branching logic on the score, you have two options. One, treat any pass as good and any fail as bad, and lean on other signals (Stripe Radar, email reputation, your own heuristics) for graded risk. Two, collect your own behavioral signals client-side and score them yourself. We did the second on one app.
▸Will Turnstile annoy my users like reCAPTCHA puzzles do?
In our traffic, no user has been shown an interactive puzzle. Turnstile uses non-interactive browser checks and most users see a brief spinner and a checkmark. Users on unusual networks or in headless browsers can get a managed challenge that asks for a click, but it is not image grid puzzles. Complaints to our support inbox about CAPTCHAs went from a few per week to zero.
▸What happens if Cloudflare has an outage?
If the Turnstile script or siteverify endpoint is down, your protected forms will fail closed unless you write fallback logic. You can choose to fail open during outages, but then you are unprotected. We accepted concentration risk because our apps already sit on Cloudflare for DNS and Workers. If your app has zero other Cloudflare dependency, factor the new failure domain into your decision.
- [1]Cloudflare Turnstile documentationdevelopers.cloudflare.com
- [2]Turnstile testing site keysdevelopers.cloudflare.com
- [3]Google reCAPTCHA Enterprisecloud.google.com
- [4]Turnstile siteverify API referencedevelopers.cloudflare.com
Related posts
How we batch-generate 50,000 cover images per month for Interior AI Designs
The actual queue, the actual costs, the actual failure modes behind running 50k FLUX renders a month for Interior AI Designs without lighting our bank account on fire.
How we batch-generate 50,000 cover images a month for Interior AI Designs
The actual pipeline behind 50k monthly room renders at Interior AI Designs: queues, FAL, idempotency keys, and the boring failure modes nobody writes about
An AI lead generation agent that books meetings while you sleep
How we build AI lead gen agents that actually book meetings overnight: the workflow, the prompts we use, the guardrails that stop it from embarrassing you.