How to Add a CAPTCHA to Your Forms (Cloudflare Turnstile, reCAPTCHA, hCaptcha)
Stop bots spamming your signup, login and contact forms. How modern CAPTCHAs work, Turnstile vs reCAPTCHA vs hCaptcha, a working Cloudflare Turnstile example with the server-side check most tutorials forget, and when a CAPTCHA isn't the right tool.
Put a form on the internet and bots will find it within days: fake signups, spam messages, password-guessing on your login page, and — if the form triggers anything paid, like an AI call or an SMS — real money lost. A CAPTCHA is a check that tries to tell humans from bots before the form is accepted.
Modern CAPTCHAs are mostly invisible
The old CAPTCHAs — squiggly letters, "click every traffic light" — are slowly disappearing. Modern ones mostly run quietly in the background, looking at signals from the browser, and only show a challenge when something seems off. Most real visitors never notice them.
The main options
| Cloudflare Turnstile | Google reCAPTCHA | hCaptcha | |
|---|---|---|---|
| Cost | Free | Free tier, paid above it | Free tier, paid features |
| Usually visible? | Rarely | v3 invisible; v2 shows a checkbox | Sometimes |
| Privacy | No ads tracking | Google service | Privacy-focused |
| Needs your site on Cloudflare? | No | — | — |
Turnstile is a popular choice for new projects: free, mostly invisible, and it works on any site, whether or not you use Cloudflare's other services.
How every CAPTCHA works: two halves
- In the browser: a widget runs its checks and produces a token — a one-time proof that the check passed. The token is submitted with your form.
- On your server: you send that token, plus your secret key, to the CAPTCHA provider's verification API. It replies "valid" or "invalid".
The second half is the one people forget. If your server doesn't verify the token, a bot can simply skip the widget and post directly to your form endpoint. A CAPTCHA that's only checked in the browser protects nothing.
Example: Cloudflare Turnstile
Create a widget in the Cloudflare dashboard to get a site key (public, goes in your HTML) and a secret key (private, server only).
In the form
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form action="/api/signup" method="POST">
<input type="email" name="email" required>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Sign up</button>
</form>
The widget adds a hidden field called cf-turnstile-response to the form, containing the token.
On the server
// app/api/signup/route.ts (Next.js)
export async function POST(req: Request) {
const form = await req.formData();
const token = form.get("cf-turnstile-response");
const result = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY!,
response: String(token ?? ""),
}),
}
).then((r) => r.json());
if (!result.success) {
return Response.json({ error: "Verification failed" }, { status: 400 });
}
// ...create the account
return Response.json({ ok: true });
}
Notes:
- The secret key lives in a server-side environment variable. (What is an API key?)
- Tokens are single-use and expire after a few minutes, so verify on every submission and let the widget refresh if the user takes a long time.
- In single-page apps, render the widget with the provider's JavaScript API or a React wrapper, and reset it after each submission.
Where to put a CAPTCHA
- Signup — stops fake accounts.
- Contact and feedback forms — stops spam. (How to add a contact form.)
- Login, at least after a few failed attempts — slows password guessing.
- Password reset and magic-link requests — stops inbox flooding and email bills.
- Anything that costs you money per request — AI generation, SMS, email.
A CAPTCHA isn't enough on its own
CAPTCHAs raise the cost for bots; determined attackers can still get through, sometimes by paying humans to solve them. Combine them with:
- Rate limiting per IP and per account. (What is rate limiting?)
- A honeypot field that only bots fill in.
- Email verification before an account can do anything significant.
- Spending limits on paid APIs. (How to stop bots from running up your AI app's bill.)
Accessibility and privacy
- Prefer invisible or "managed" modes — visual puzzles are hard or impossible for some disabled users. (Web accessibility basics.)
- A CAPTCHA processes data about your visitors; mention it in your privacy policy.
The summary
- A CAPTCHA gives the browser a token that your server must verify with the provider.
- Turnstile is free and mostly invisible; reCAPTCHA and hCaptcha are alternatives.
- Always verify the token server-side, with the secret key kept on the server.
- Use it on signup, login, contact and anything paid — alongside rate limiting.
EasySpawn runs your backend with secrets stored as server-side environment variables, so your CAPTCHA secret stays where it belongs — and Claude Code can wire up and test the server-side check. See how it works or join the waitlist.
Related: What Is a DDoS Attack? · Form Validation Explained · How to Add Login to an AI-Built App · Magic Link Login
Keep reading
How to Add a Contact Form to Your Website (With or Without a Backend)
Four ways to make a contact form actually deliver messages — form services, your host's built-in forms, Google Forms, and your own backend route — plus how to stop spam and why you should never put an email API key in frontend code.
localStorage vs sessionStorage vs Cookies: Where Should You Store It?
Three ways to store data in the browser, with different lifetimes, sizes and security properties. What each one is for, why login tokens shouldn't go in localStorage, the size limits, and a simple rule for choosing.