Skip to content

Cloudflare Turnstile: The CAPTCHA That Doesn’t Make Your Users Hate You

reCAPTCHA hurts conversion. Turnstile doesn’t. Here’s the wiring that keeps your forms spam-free without the click-the-bicycles ritual.

John Cravey with AIFounder4 min readUpdated Jul 6, 2026

Every public form on an FH client site has Cloudflare Turnstile validating it server-side. We migrated from reCAPTCHA two years ago and never looked back. Turnstile is invisible to the user 99% of the time, blocks the same bot traffic, is free, and respects user privacy in a way reCAPTCHA doesn’t. Here’s the integration we run across the client book.

Free estimate · 2 minutes

Slow pages lose rankings and leads. Price the fix.

Build the estimate to see what a fast rebuild would look like for your site and what we would ship in the first thirty days.

Why reCAPTCHA hurts you

reCAPTCHA v2 (the “I’m not a robot” checkbox + bicycle-puzzle) demonstrably hurts conversion rate. We A/B tested it on three client lead forms. The conversion drop ranged from 8% to 18%. The frustration tax compounds: every user who fails the puzzle once is less likely to retry, especially on mobile.

reCAPTCHA v3 (score-based, invisible) is better but Google still tracks every page where it’s embedded. For sites taking marketing claims about privacy seriously, that’s a problem.

What Turnstile does differently

Turnstile runs a few non-invasive checks in the user’s browser (browser fingerprint, environment consistency, behavioral signals) and produces a token. The token is sent with the form submission. Your server validates the token with Cloudflare’s API. No tracking, no puzzles, no Google. The full check takes ~200ms and is invisible.

When Turnstile is unsure, it falls back to a managed challenge — a single click, no puzzle. We see this on <1% of submissions across the FH client book.

Step 1: create a Turnstile site key

Cloudflare dashboard → Turnstile → Add Site. Pick the “Managed” mode (lets Cloudflare decide invisible vs. challenge). You get a site key (public) and a secret key (server-only). Add the domain(s) the form will live on.

Step 2: render the widget in your form

The widget is a `<div>` with a Cloudflare-loaded script. On submit, the widget produces a hidden form field containing the token.

// app/contact/page.tsx (server component)
import { ContactForm } from "./ContactForm";

export default function Contact() {
  return <ContactForm siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!} />;
}

// app/contact/ContactForm.tsx
"use client";
import Script from "next/script";
import { submitLead } from "./actions";

export function ContactForm({ siteKey }: { siteKey: string }) {
  return (
    <>
      <Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer />
      <form action={submitLead}>
        <input name="name" required />
        <input name="email" type="email" required />
        <div className="cf-turnstile" data-sitekey={siteKey} />
        <button>Send</button>
      </form>
    </>
  );
}

Step 3: verify the token server-side

The widget puts a token in a hidden field called `cf-turnstile-response`. Your server action reads it, POSTs it to Cloudflare’s siteverify endpoint, and only inserts the lead if the response is successful.

// app/contact/actions.ts
"use server";
import "server-only";
import { z } from "zod";

const LeadSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  "cf-turnstile-response": z.string().min(1),
});

async function verifyTurnstile(token: string, ip: string | null): Promise<boolean> {
  const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      secret: process.env.TURNSTILE_SECRET_KEY!,
      response: token,
      ...(ip ? { remoteip: ip } : {}),
    }),
  });
  const data = await res.json();
  return data.success === true;
}

export async function submitLead(formData: FormData) {
  const parsed = LeadSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { ok: false };
  const ok = await verifyTurnstile(parsed.data["cf-turnstile-response"], null);
  if (!ok) return { ok: false, reason: "turnstile" };
  // insert lead
  return { ok: true };
}

What to do when verification fails

Three options. (1) Silent drop: return `{ ok: true }` and discard the submission. Confusing to legitimate users who somehow failed. (2) Hard error: tell the user verification failed and ask them to retry. Honest but annoying. (3) Soft challenge: ask them to retry the form. We default to option 2 with a friendly error message — “Hmm, we couldn’t verify that submission. Please refresh and try again.” In practice fewer than 1 in 1000 legitimate submissions need this.

Mobile considerations

Turnstile works on mobile out of the box — no extra config. It’s slightly slower (the fingerprint checks run longer) but still under 500ms in our measurements. If you’re seeing complaints about mobile form latency, check the images on the page before blaming Turnstile.

Hidden form fields and honeypots

Turnstile is enough on its own. But adding a honeypot field — an invisible input that bots fill in and humans don’t — catches the cheapest bots before they even hit Turnstile. We add a `<input name="website" tabindex=-1 style="position:absolute;left:-9999px">` and reject any submission where it’s non-empty.

Migrating from reCAPTCHA

Three steps: (1) Replace the reCAPTCHA script tag with the Turnstile one. (2) Replace `<div class="g-recaptcha">` with `<div class="cf-turnstile">`. (3) Replace the server-side verification call. Total time: 30 minutes per form. Watch your spam volume for a week to confirm the change doesn’t miss anything.

Cost

Free up to 10M solves per month. We’re not close. Premium tiers add features we don’t need.

When Turnstile isn’t enough

For high-value targets (financial logins, healthcare patient portals, anything where credential stuffing is worth attacker time), pair Turnstile with: rate-limiting at the edge (Cloudflare WAF rule), 2FA on the account, and behavioral anomaly detection. Turnstile is the front line; it’s not the only line.

How this lands across FH client work

Every public form on every FH client site uses Turnstile. Zero spam complaints in 18 months. Zero conversion drop attributable to verification. If you’re still on reCAPTCHA, book a consultation — the migration is a 30-minute change per form with a measurable conversion lift.

Answers

Frequently asked questions

What is wrong with traditional CAPTCHA?

It taxes every legitimate user to stop some bots, and the tax is highest for the people least able to pay it: mobile users, people with disabilities, and anyone in a hurry. Measured on completed forms rather than blocked bots, it frequently costs more than it saves.

What does Turnstile do differently?

It runs a set of non-interactive checks and only escalates to a challenge when something looks wrong, so most visitors see nothing at all. The trade is the same as any invisible check: you accept a small false-negative rate in exchange for not punishing everyone.

Is the widget alone enough?

No. The token it produces must be verified server-side before the submission is accepted. A form that renders the widget and never verifies is decorative, and it is a surprisingly common implementation because the client half is the visible half.

What should happen when verification fails?

Reject the submission and tell the user plainly, with a way to retry. Silently discarding a failed submission produces the worst outcome: a real customer believes they contacted you and nobody did anything, which is indistinguishable from being ignored.

Do I still need a honeypot field?

Yes, and it costs nothing. A hidden field that humans never fill catches the simplest automation before any verification runs. Layered cheap checks beat one strong check, because the cheap ones remove the volume and leave the harder cases for the real verification.

How does this behave on mobile?

Better than interactive challenges, which is most of the argument. Image-selection challenges on a phone are where form completion goes to die, and replacing them with a check that usually renders nothing removes a real conversion loss rather than a theoretical one.

How do I migrate from an existing CAPTCHA?

Add the new widget and its server verification alongside the old one, confirm real submissions pass, then remove the old one in a separate change. Swapping in one commit means an outage on your lead form if anything about the verification is wrong.

What does it cost?

Nothing at the volumes a small-business form produces, which removes the usual argument for tolerating a worse experience. Cost is rarely the reason a site keeps a hostile challenge; it is that nobody has revisited the decision since it was made.

When is this not enough?

Against determined targeted abuse, where a human is being paid to fill your form, and against attacks that skip the form entirely by posting to the endpoint. Rate limiting and server-side validation cover the second, and no widget covers the first.

Does it affect accessibility?

Favourably, because the common path presents no challenge at all. The cases that do escalate still need to be usable, so check the fallback path with a keyboard and a screen reader rather than assuming the invisible case is the only one your users will hit.

Should every form have one?

Every public form that creates something: leads, signups, comments. Internal forms behind authentication do not need it, and adding it there is friction with no threat model behind it. The rule is public and unauthenticated, not simply a form.

How do I know it is working?

Compare submission volume and quality before and after. A drop in total submissions with steady real enquiries means it is doing its job. A drop in both means it is blocking customers, which is the outcome nobody checks for because the spam did go away.

Question we did not answer? Ask us directly and we will answer it here.

John Cravey, Founder
Written by
John Cravey
Founder

Founder of Frontend Horizon. Writes most of the long-form work on the FH blog.

Newer post
Accessibility Law in 2026: The Lawsuit Landscape and the Compliant Build Posture
Older post
Google Analytics 4 Consent Mode v2: The Implementation That Doesn’t Break Your Data
Keep reading

More from the blog

Next.js·10 min

Speed Is a Ranking Factor: The Next.js Performance Checklist by Business Size

Google measures your speed with real users and ranks you on it. Next.js hands you the tools to win. Most sites leave them switched off.

Next.js·6 min

Next.js 16.1 in Production: The Migration Playbook We Run on Every FH Site

Next 16.1 is the lean target. Here’s the exact migration we run, what breaks, and what to delete after.

Cloudflare·6 min

Cloudflare DNS and CDN: The Base Configuration for Every FH Client Site

Every FH site sits behind Cloudflare. Here’s the exact configuration and why each setting is where it is.