Turnstile vs reCAPTCHA vs hCaptcha: CAPTCHA and Bot Protection (2026)
TL;DR
Cloudflare Turnstile is the privacy-first CAPTCHA alternative — invisible challenges, no visual puzzles, free, doesn't sell user data, uses Cloudflare's bot detection signals. reCAPTCHA is Google's CAPTCHA — reCAPTCHA v3 scores user behavior invisibly, v2 shows "I'm not a robot" checkbox, the most widely used, free with Google's data collection. hCaptcha is the privacy-focused CAPTCHA — visual challenges, site owners earn credits, GDPR-compliant, used by Cloudflare (before Turnstile) and Discord. In 2026: Turnstile for invisible privacy-first bot protection, reCAPTCHA for maximum bot detection with Google's network, hCaptcha for privacy-respecting visual challenges.
Key Takeaways
- Turnstile: Free — invisible, no puzzles, privacy-first, Cloudflare network signals
- reCAPTCHA: Free (with data) — v3 invisible scoring, v2 checkbox, Google ecosystem
- hCaptcha: Free tier — visual challenges, privacy-focused, GDPR-compliant, earns credits
- Turnstile never shows visual puzzles to users — fully invisible
- reCAPTCHA v3 is also invisible but sends data to Google
- hCaptcha is the only one where site owners can earn money
Cloudflare Turnstile
Turnstile — invisible bot protection:
Client-side widget
<!-- Add Turnstile widget: -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form action="/api/submit" method="POST">
<input type="text" name="email" placeholder="Email" />
<!-- Turnstile widget (renders invisibly): -->
<div class="cf-turnstile"
data-sitekey="0x4AAAAAAA..."
data-theme="dark"
data-callback="onTurnstileSuccess">
</div>
<button type="submit">Submit</button>
</form>
<script>
function onTurnstileSuccess(token) {
console.log("Turnstile token:", token)
}
</script>
React integration
import { useRef, useEffect, useState } from "react"
function TurnstileWidget({ siteKey, onVerify }: {
siteKey: string
onVerify: (token: string) => void
}) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!ref.current || !window.turnstile) return
const widgetId = window.turnstile.render(ref.current, {
sitekey: siteKey,
callback: onVerify,
theme: "dark",
})
return () => window.turnstile.remove(widgetId)
}, [siteKey, onVerify])
return <div ref={ref} />
}
// Usage:
function ContactForm() {
const [token, setToken] = useState("")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await fetch("/api/contact", {
method: "POST",
body: JSON.stringify({ token, /* form data */ }),
})
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<TurnstileWidget
siteKey="0x4AAAAAAA..."
onVerify={setToken}
/>
<button type="submit" disabled={!token}>Send</button>
</form>
)
}
Server-side verification
// Verify Turnstile token on server:
async function verifyTurnstile(token: string, ip?: string): Promise<boolean> {
const response = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET_KEY,
response: token,
remoteip: ip,
}),
}
)
const data = await response.json()
return data.success === true
}
// Express route:
app.post("/api/contact", async (req, res) => {
const { token, email, message } = req.body
const valid = await verifyTurnstile(token, req.ip)
if (!valid) {
return res.status(403).json({ error: "Bot detected" })
}
// Process form...
res.json({ success: true })
})
reCAPTCHA
reCAPTCHA — Google bot detection:
reCAPTCHA v3 (invisible scoring)
<!-- Load reCAPTCHA v3: -->
<script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>
<script>
grecaptcha.ready(function() {
// Execute on form submit:
document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault()
const token = await grecaptcha.execute("SITE_KEY", {
action: "submit_form",
})
// Send token to server:
await fetch("/api/submit", {
method: "POST",
body: JSON.stringify({ token }),
})
})
})
</script>
React integration (v3)
import { useCallback } from "react"
// Load reCAPTCHA v3 script in layout:
// <script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>
function useRecaptcha(siteKey: string) {
const execute = useCallback(async (action: string): Promise<string> => {
return new Promise((resolve) => {
window.grecaptcha.ready(async () => {
const token = await window.grecaptcha.execute(siteKey, { action })
resolve(token)
})
})
}, [siteKey])
return { execute }
}
// Usage:
function LoginForm() {
const { execute } = useRecaptcha("SITE_KEY")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const token = await execute("login")
await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ token, /* credentials */ }),
})
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Log In</button>
</form>
)
}
Server-side verification
// Verify reCAPTCHA token:
async function verifyRecaptcha(token: string): Promise<{
success: boolean
score: number
action: string
}> {
const response = await fetch(
"https://www.google.com/recaptcha/api/siteverify",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: process.env.RECAPTCHA_SECRET_KEY!,
response: token,
}),
}
)
return response.json()
}
// Express route with score threshold:
app.post("/api/login", async (req, res) => {
const { token, email, password } = req.body
const result = await verifyRecaptcha(token)
if (!result.success || result.score < 0.5) {
return res.status(403).json({ error: "Bot detected", score: result.score })
}
// Score 0.0 = likely bot, 1.0 = likely human
if (result.score < 0.7) {
// Low confidence — add additional verification
return res.json({ requireMFA: true })
}
// High confidence — proceed normally
// Authenticate user...
})
reCAPTCHA v2 (checkbox)
<!-- "I'm not a robot" checkbox: -->
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
<form action="/api/submit" method="POST">
<input type="email" name="email" />
<div class="g-recaptcha" data-sitekey="SITE_KEY" data-theme="dark"></div>
<button type="submit">Submit</button>
</form>
hCaptcha
hCaptcha — privacy-focused CAPTCHA:
Client-side widget
<!-- Load hCaptcha: -->
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<form action="/api/submit" method="POST">
<input type="email" name="email" />
<div class="h-captcha"
data-sitekey="SITE_KEY"
data-theme="dark"
data-size="normal">
</div>
<button type="submit">Submit</button>
</form>
React integration
import HCaptcha from "@hcaptcha/react-hcaptcha"
import { useRef, useState } from "react"
function SignupForm() {
const [token, setToken] = useState("")
const captchaRef = useRef<HCaptcha>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!token) {
// Trigger captcha:
captchaRef.current?.execute()
return
}
await fetch("/api/signup", {
method: "POST",
body: JSON.stringify({ token, /* form data */ }),
})
// Reset after submission:
captchaRef.current?.resetCaptcha()
setToken("")
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<HCaptcha
ref={captchaRef}
sitekey="SITE_KEY"
theme="dark"
onVerify={setToken}
onExpire={() => setToken("")}
onError={() => setToken("")}
/>
<button type="submit">Sign Up</button>
</form>
)
}
Invisible mode
import HCaptcha from "@hcaptcha/react-hcaptcha"
function InvisibleForm() {
const captchaRef = useRef<HCaptcha>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// Triggers invisible challenge:
captchaRef.current?.execute()
}
const onVerify = async (token: string) => {
await fetch("/api/submit", {
method: "POST",
body: JSON.stringify({ token }),
})
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<HCaptcha
ref={captchaRef}
sitekey="SITE_KEY"
size="invisible"
onVerify={onVerify}
/>
<button type="submit">Submit</button>
</form>
)
}
Server-side verification
// Verify hCaptcha token:
async function verifyHCaptcha(token: string, ip?: string): Promise<boolean> {
const response = await fetch("https://api.hcaptcha.com/siteverify", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: process.env.HCAPTCHA_SECRET_KEY!,
response: token,
...(ip && { remoteip: ip }),
}),
})
const data = await response.json()
return data.success === true
}
// Express route:
app.post("/api/signup", async (req, res) => {
const { token, email, password } = req.body
const valid = await verifyHCaptcha(token, req.ip)
if (!valid) {
return res.status(403).json({ error: "CAPTCHA verification failed" })
}
// Create account...
res.json({ success: true })
})
Feature Comparison
| Feature | Turnstile | reCAPTCHA | hCaptcha |
|---|---|---|---|
| Provider | Cloudflare | Intuition Machines | |
| Invisible mode | ✅ (always) | ✅ (v3) | ✅ (invisible size) |
| Visual puzzles | Never | v2 only | ✅ (default) |
| Privacy | ✅ (no data selling) | ❌ (Google data) | ✅ (GDPR-compliant) |
| Pricing | Free | Free | Free tier + paid |
| Score-based | ❌ (pass/fail) | ✅ (v3: 0.0–1.0) | ❌ (pass/fail) |
| React package | Community | Community | ✅ (official) |
| Accessibility | ✅ | ✅ | ✅ |
| GDPR-compliant | ✅ | ⚠️ (requires consent) | ✅ |
| Enterprise | Turnstile Pro | reCAPTCHA Enterprise | hCaptcha Enterprise |
| Revenue share | ❌ | ❌ | ✅ (site owners earn) |
| Adoption | Growing fast | Most widely used | Major sites (Discord) |
When to Use Each
Use Turnstile if:
- Want invisible bot protection with zero user friction
- Care about user privacy and GDPR compliance
- Already using Cloudflare for your infrastructure
- Want a completely free solution with no data trade-offs
Use reCAPTCHA if:
- Need score-based bot detection (v3 risk scoring)
- Want the most battle-tested bot detection
- Already in the Google ecosystem
- Need to differentiate between "likely human" and "definitely bot"
Use hCaptcha if:
- Want privacy-focused CAPTCHA with visual challenges
- Want to earn revenue from CAPTCHA impressions
- Need strict GDPR compliance
- Building for users who prefer visible security confirmation
Methodology
Feature comparison based on Cloudflare Turnstile, Google reCAPTCHA v2/v3, and hCaptcha as of March 2026.
Compare security tooling and frontend libraries on PkgPulse →