CAPTCHA strategies
When to show CAPTCHA, which one, and how to keep UX
CAPTCHAs annoy real users. Bots are increasingly able to solve them. Strategy: show CAPTCHA rarely, only when needed, choosing one that's user-friendly.
When to show
Always
- Signup (low cost, high abuse potential).
Risk-based
- Login: only after N failed attempts.
- Password reset: only after N requests in a window.
- Sensitive endpoints: based on risk score.
function shouldShowCaptcha(req, identity) {
if (req.path === "/register") return true; // always for signup
const failedAttempts = countFailedAttempts(req.ip);
if (failedAttempts > 3) return true;
const riskScore = computeRisk(req, identity);
if (riskScore > 50) return true;
return false;
}Which CAPTCHA
Cloudflare Turnstile (recommended)
- Privacy-respecting.
- Free.
- Often passes without user interaction.
- Doesn't track / build profile.
<div class="cf-turnstile" data-sitekey="..." data-callback="onVerify"></div>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"></script>Server-side verify:
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
method: "POST",
body: new URLSearchParams({ secret: TURNSTILE_SECRET, response: turnstileToken }),
});
const { success } = await res.json();hCaptcha
- Open alternative to reCAPTCHA.
- Free for low volume.
- Some user-tracking concerns (compared to Turnstile).
Google reCAPTCHA v3
- Score-based (you get a 0-1 score).
- Heavy user tracking.
- "Free" but Google profile users.
For privacy-conscious deployments: Turnstile or hCaptcha. Avoid reCAPTCHA v3 unless your customers expect it.
Self-hosted
- Anubis, proof-of-work CAPTCHA, server-resource intensive but private.
- Honeypot fields (no user-visible challenge but catches bots).
Honeypot field
<input type="text" name="address_line_2" tabindex="-1" autocomplete="off"
style="position: absolute; left: -9999px;">Real users won't fill. Bots that blindly fill all fields will.
Server-side:
if (req.body.address_line_2) {
return Response.json({ error: "spam_detected" }, 400);
}Catches dumb bots. Sophisticated ones inspect tabindex / CSS and skip.
Layered approach
1. Honeypot (always on, catches 80% of bot traffic).
2. Cloudflare bot fight (passive, layer 4).
3. Rate limit per IP.
4. Turnstile if risk score elevated.
5. Account lockout / reCAPTCHA challenge if hits keep coming.Each layer is cheap for legitimate users; expensive for attackers.
UX
Where to place
Above the submit button, after all fields. Not in middle of form.
Failed verification
Show clear message:
"We couldn't verify you're human. Please try again."
[Refresh challenge]Don't punish: don't lock the account, don't add to suspect lists. Real users sometimes fail (network glitch, slow widget load).
Loading time
Turnstile loads fast (~200ms). reCAPTCHA can be slower. Set a timeout:
setTimeout(() => {
if (!turnstileVerified) {
// fall back to manual challenge
}
}, 5000);Multiple submits
If user submits, fails CAPTCHA, retries: must get a fresh token. Old token can't be reused.
// Always refresh on submit:
turnstile.execute(widgetId);Accessibility
- Audio CAPTCHA for vision-impaired (Google reCAPTCHA has it; Turnstile doesn't yet).
- Don't make the challenge time-out so quickly that screen-reader users miss it.
- ARIA labels on widgets.
Test with screen readers.
Headless / API endpoints
For API consumers (no browser), CAPTCHA doesn't apply. Use API keys or OAuth2 client credentials instead.
Don't apply browser CAPTCHA to programmatic endpoints, useless friction.
When CAPTCHA is the wrong tool
For:
- Persistent abuse: ban the IP/ASN.
- Volume attacks: rate limit at the LB.
- Identity-confirmation: use real verification (ID upload).
CAPTCHA is for medium-confidence "is this human?" Not for "is this the right human?"
Performance impact
CAPTCHA widgets are external script loads. They add ~200ms latency. For login (which should feel snappy), only load conditionally:
const [showCaptcha, setShowCaptcha] = useState(false);
function onSubmit() {
if (failedAttempts > 2 && !showCaptcha) {
setShowCaptcha(true);
return;
}
// submit
}
{showCaptcha && <Turnstile siteKey={...} />}Loaded only when needed.
Cost
- Cloudflare Turnstile: free.
- hCaptcha: free up to 1M calls/mo.
- reCAPTCHA: free, but you pay in privacy.
No reason to pay for CAPTCHA at typical Olympus scale.