Cloudflare Turnstile is a smart, CAPTCHA-free alternative that verifies visitors are human without requiring them to solve visual puzzles. It runs a series of non-interactive JavaScript challenges (like proof-of-work and space) to gather signals about the browser environment.
You can embed Turnstile into any application to stop automated abuse, such as spam submissions on contact forms or brute-force login attempts, without routing your traffic through Cloudflare's CDN.
Traditional CAPTCHAs frustrate users. Identifying traffic lights or deciphering distorted text introduces friction that harms conversion rates. Turnstile solves this by moving the verification fully into the background.
It offers three widget modes:
For maximum user experience, the Invisible mode is ideal. The challenge executes as soon as the user accesses the page or triggers an event (like scrolling to a form), ensuring the token is ready by the time they submit.
Beyond basic contact forms, Turnstile handles multiple scenarios:
Before writing any code, you need to get started by generating your authentication keys from Cloudflare. Turnstile can be used independently; you are not required to proxy your site's traffic through Cloudflare's CDN.
localhost for local development).Here is a full-stack implementation using React for the frontend and a Cloudflare Worker (via Hono) for the backend API. For other languages and frameworks, refer to the official Turnstile tutorials.
In this example, we execute the Turnstile verification silently when the contact form mounts. Notice that we append ?render=explicit&onload=onloadTurnstileCallback to the script path. This disables implicit DOM scanning, ensuring you retain strict programmatic control over the widget lifecycle - an essential requirement for Single Page Applications (SPAs) like React.
import { useEffect, useRef, useState } from "react"; // Extend the Window interface for Turnstile declare global { interface Window { turnstile: any; onloadTurnstileCallback: () => void; } } export function ContactForm() { const turnstileRef = useRef<HTMLDivElement>(null); const [token, setToken] = useState<string>(""); const [widgetId, setWidgetId] = useState<string>(""); useEffect(() => { // Dynamically load the Turnstile script const script = document.createElement("script"); script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onloadTurnstileCallback"; script.async = true; script.defer = true; document.head.appendChild(script); // Initialize the invisible widget once loaded window.onloadTurnstileCallback = () => { const id = window.turnstile.render(turnstileRef.current, { sitekey: "YOUR_PUBLIC_SITE_KEY", theme: "light", size: "invisible", callback: function(token: string) { setToken(token); }, "error-callback": function() { // Handle network errors or challenge failures window.turnstile.reset(id); } }); setWidgetId(id); }; return () => { document.head.removeChild(script); delete window.onloadTurnstileCallback; }; }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!token) { alert("Security check pending. Please wait a moment."); return; } try { // Submit form data along with the Turnstile token const response = await fetch("/api/contact", { method: "POST", body: JSON.stringify({ token, /* other fields */ }), headers: { "Content-Type": "application/json" } }); const data = await response.json(); if (!data.success) { alert("Submission failed. Please try again."); // Reset turnstile to generate a fresh token for retry window.turnstile.reset(widgetId); } else { alert("Success!"); } } catch (err) { // Reset on network errors window.turnstile.reset(widgetId); } }; return ( <form onSubmit={handleSubmit}> {/* Invisible Turnstile Container */} <div ref={turnstileRef} className="hidden"></div> <input type="text" name="name" placeholder="Your Name" required /> <input type="email" name="email" placeholder="Your Email" required /> <button type="submit">Submit</button> </form> ); }
Your server must validate the token with Cloudflare's /siteverify endpoint for strict server-side validation before processing the request. This TypeScript class handles the validation cleanly.
// turnstile-validator.ts export type TurnstileResponse = { success: boolean; challenge_ts?: string; hostname?: string; "error-codes"?: string[]; action?: string; }; export class TurnstileValidator { constructor(private secretKey: string, private timeout = 10000) {} async validate(token: string, remoteip?: string): Promise<TurnstileResponse | { success: false, error: string }> { if (!token || typeof token !== "string") { return { success: false, error: "Invalid token format" }; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const formData = new FormData(); formData.append("secret", this.secretKey); formData.append("response", token); if (remoteip) formData.append("remoteip", remoteip); const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body: formData, signal: controller.signal, }); return await response.json() as TurnstileResponse; } catch (error) { return { success: false, error: "Validation failed" }; } finally { clearTimeout(timeoutId); } } }
Wire up the validator in your API route. Here is an example using the Hono framework on a Cloudflare Worker.
// routes.ts import { Hono } from "hono"; import { TurnstileValidator } from "./turnstile-validator"; export function createContactRoutes(app: Hono<{ Bindings: Env }>) { app.post("/api/contact", async (c) => { const formData = await c.req.json(); const token = formData.token; const validator = new TurnstileValidator(c.env.TURNSTILE_SECRET_KEY); const clientIP = c.req.header("CF-Connecting-IP") || "unknown"; const validation = await validator.validate(token, clientIP); if (!validation.success) { return c.json({ success: false, message: "Security validation failed." }, 400); } // Process the form submission... return c.json({ success: true, message: "Message sent successfully!" }); }); }
Turnstile is hosted under challenges.cloudflare.com. If your website has a strict Content Security Policy (CSP) in place, you must add directives to allow connections to this origin, otherwise the script and interactive frames will be blocked.
Here is an example applying this using a Hono middleware:
// index.ts import { Hono } from "hono"; import { createContactRoutes } from "./routes"; const app = new Hono<{ Bindings: Env }>(); app.use("*", async (c, next) => { await next(); // Set CSP headers for all responses to allow Turnstile scripts, frames, and connections c.header( "Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com;" ); }); createContactRoutes(app); export default app;
To ensure your application is fully protected, keep these operational and security principles in mind:
/siteverify API to confirm it has not been tampered with. Without this step, your implementation is incomplete.Before deploying to production, ensure you aren't disrupting actual user flows. Turnstile provides special "dummy" testing keys that behave predictably:
1x00000000000000000000AA) which will always successfully generate a token or force an interactive challenge for design testing, without actually skewing your Turnstile analytics.1x0000000000000000000000000000000AA which always passes, or 2x0000000000000000000000000000000AA which always fails). Note that your production Secret Key will reject test tokens.If you are already running reCAPTCHA or hCaptcha, migrating to Turnstile is incredibly easy. In most cases, Turnstile serves as a drop-in replacement. You can typically copy and paste the Turnstile script right where your existing CAPTCHA implementation lives and adjust the callback definitions.
When using Turnstile in invisible mode, visitors will not see the Cloudflare branding or terms. It is a strict requirement to embed the Turnstile Privacy Addendum in your own website's privacy policy.
Is Cloudflare Turnstile free? Yes. Turnstile's Free plan allows up to 20 widgets and unlimited challenges. It provides WCAG 2.2 AAA compliance out-of-the-box and covers most personal blogs and mid-sized businesses.
Does Turnstile hurt my site's performance? No. The challenges are lightweight JavaScript routines that run asynchronously. Using the invisible widget size ensures zero UI shifting or layout jumps.
What happens if the challenge fails?
Turnstile allows you to handle failures gracefully. The /siteverify response includes specific error codes. You could conditionally fallback to a visible Managed challenge if the invisible mode continually fails.
About the Author
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Related Blog Posts
Enjoyed this blog post? Check out these related posts!

Blocked by CORS in FastAPI? Here's How to Fix It
Solving Cross-Origin Errors Between Your Frontend and FastAPI
Read More...

Serving a React Frontend Application with FastAPI
A Guide to Serving Your Frontend and Backend from a Single Application
Read More...

A Practical Guide to FastAPI Security
A Comprehensive Checklist for Production-Ready Security for a FastAPI Application
Read More...

How to Protect Your FastAPI OpenAPI/Swagger Docs with Authentication
A Guide to Securing Your API Documentation with Authentication
Read More...
On this page
Back to Blogs
Contact Me
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.