Turnstile — Privacy-First CAPTCHA Alternative
In this tutorial, you'll learn about Turnstile. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Cloudflare Turnstile is a privacy-preserving CAPTCHA alternative that replaces traditional puzzle-based challenges with a lightweight, invisible verification that does not track users across sites.
What You Will Learn
You will learn how Turnstile verifies human visitors without friction, how to integrate it into web forms using the JavaScript widget or API, and how to verify tokens server-side in multiple programming languages.
Why It Matters
Traditional CAPTCHAs frustrate users, fail Accessibility standards, and collect personal data. Turnstile provides better user experience, stronger bot detection, and complete privacy Compliance — all without showing a single puzzle.
Real-World Use Case
A SaaS landing page replaced Google reCAPTCHA with Turnstile on their signup form. Conversion rates increased by 12% because users no longer had to solve image puzzles. The company also eliminated third-party cookies from their signup flow, simplifying GDPR Compliance.
How Turnstile Works
Turnstile uses a combination of browser fingerprinting, behaviour analysis, and light challenges to determine whether a Visitor is human — all behind the scenes without visible interaction.
flowchart LR
A[Visitor Loads Form] --> B[Turnstile Loads in Background]
B --> C{Analyse Browser Signals}
C -->|Confident Human| D[Invisible Token Generated]
C -->|Uncertain| E[Lightweight Challenge]
E -->|Pass| D
E -->|Fail| F[Blocked]
D --> G[Form Submitted with Token]
G --> H[Server Verifies Token]
H -->|Valid| I[Process Form]
H -->|Invalid| J[Reject]
Basic HTML Integration
Add Turnstile to a form with two minutes of work:
<!DOCTYPE html>
<html>
<head>
<title>Signup with Turnstile</title>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
<form id="signup" action="/api/signup" method="POST">
<input type="email" name="email" placeholder="Email" required>
<div class="cf-turnstile" data-sitekey="SITE_KEY"></div>
<button type="submit">Sign Up</button>
</form>
</body>
</html>
Expected behaviour: The Turnstile widget renders as a checkbox or invisible challenge. Users rarely need to interact with it. The form sends a cf-turnstile-response token on submission.
Server-Side Token Verification (Node.js)
const fetch = require("node-fetch");
async function verifyTurnstileToken(token) {
const secret = process.env.TURNSTILE_SECRET_KEY;
const resp = await fetch(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
secret: secret,
response: token,
}),
}
);
const data = await resp.json();
return data.success;
}
// Usage in an Express endpoint
app.post("/api/signup", async (req, res) => {
const token = req.body["cf-turnstile-response"];
if (!token) {
return res.status(400).json({ error: "Missing Turnstile token" });
}
const isValid = await verifyTurnstileToken(token);
if (!isValid) {
return res.status(403).json({ error: "Bot detected" });
}
// Process signup...
res.json({ success: true });
});
Expected output for a valid token:
{"success": true}
Server-Side Verification (Python Flask)
import os
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = os.environ["TURNSTILE_SECRET_KEY"]
@app.route("/api/signup", methods=["POST"])
def signup():
token = request.form.get("cf-turnstile-response")
if not token:
return jsonify({"error": "Missing token"}), 400
resp = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={"secret": SECRET_KEY, "response": token},
)
result = resp.json()
if not result.get("success"):
return jsonify({"error": "Bot detected"}), 403
return jsonify({"success": True})
Expected output for verification:
{"success": true}
Server-Side Verification (Go)
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
)
func verifyTurnstile(token string) (bool, error) {
secret := os.Getenv("TURNSTILE_SECRET_KEY")
resp, err := http.PostForm(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
url.Values{"secret": {secret}, "response": {token}},
)
if err != nil {
return false, err
}
defer resp.Body.Close()
var result struct {
Success bool `json:"success"`
}
json.NewDecoder(resp.Body).Decode(&result)
return result.Success, nil
}
func handler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", 405)
return
}
token := r.FormValue("cf-turnstile-response")
valid, _ := verifyTurnstile(token)
if !valid {
http.Error(w, `{"error":"bot detected"}`, 403)
return
}
w.Write([]byte(`{"success":true}`))
}
func main() {
http.HandleFunc("/api/signup", handler)
fmt.Println("Server on :8080")
http.ListenAndServe(":8080", nil)
}
Invisible Mode Configuration
For the best user experience, use invisible mode where Turnstile never shows a widget:
<form action="/api/login" method="POST">
<div class="cf-turnstile" data-sitekey="SITE_KEY" data-size="invisible"></div>
<button type="submit">Login</button>
</form>
<script>
// Manually trigger the challenge on form submit
document.querySelector("form").addEventListener("submit", function(e) {
e.preventDefault();
turnstile.execute();
});
function turnstileCallback(token) {
document.querySelector("form").submit();
}
</script>
Common Mistakes
| Mistake | Consequence |
|---|---|
| Using the site key for server verification | Verification always fails |
| Not validating tokens server-side | Anyone can bypass by faking form submit |
Setting data-size="invisible" without calling execute |
Token never generated |
| Forgetting to include the Turnstile script | Widget does not render |
| Not handling token expiry (tokens expire after 300s) | Long forms fail on submit |
Practice Questions
- How does Turnstile differ from traditional CAPTCHA in terms of user experience?
- Why must token verification happen server-side rather than in the browser?
- What is the maximum time a Turnstile token remains valid?
Challenge
Build a complete login page with Turnstile in invisible mode. Include client-side rendering with a manual execute call and server-side token verification in your language of choice. Test with both a real browser and curl to confirm bots are blocked.
Real-World Task
Your SaaS application currently uses reCAPTCHA v2 on the signup form. Replace it with Turnstile in invisible mode. Implement the HTML widget on the frontend and server-side verification in your backend. A/B test the conversion rate difference between reCAPTCHA and Turnstile over one week.
FAQ
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security-first tools for the modern web.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro