Home

Blog

Home

Blog

Cloudflare Turnstile: How to Block Bots Invisibly in 2026

10 min read
A conceptual illustration showing Cloudflare Turnstile invisibly blocking bot traffic from a web application without a CAPTCHA.
By David Muraya • April 17, 2026

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.

Why Choose Turnstile over Traditional CAPTCHAs?

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:

  • Managed: Shows a checkbox only if the risk level is high.
  • Non-Interactive: Shows a loading spinner while validating.
  • Invisible: Runs completely in the background without any visible UI.

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.

Practical Use Cases for Invisible Turnstile

Beyond basic contact forms, Turnstile handles multiple scenarios:

  • Authentication flows: Block credential stuffing on login and password reset pages.
  • E-commerce checkout: Prevent inventory hoarding bots from adding items to carts.
  • Public AI inference: Secure public endpoints that run expensive LLM completions on Cloudflare Workers AI to prevent abuse of your compute resources.
  • API rate limiting: Protect public single-page application (SPA) endpoints using Pre-Clearance cookies (pair this with a strong backend rate limiting strategy).

Step 0: Setting Up Cloudflare Turnstile

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.

  1. Log in to the Cloudflare Dashboard (you can sign up for a free account if you don't have one).
  2. Navigate to Turnstile on the left-side navigation menu.
  3. Click Add site and input your website's domain name. (Tip: You can add localhost for local development).
  4. For the Widget Mode, make sure to select Invisible. This ensures the challenge will run completely in the background.
  5. Once created, Cloudflare will provide you with a Site Key and a Secret Key.
    • The Site Key is public and goes in your frontend React code.
    • The Secret Key must remain securely hidden on your backend server.

Implementing Invisible Turnstile: A Production-Ready Example

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.

1. Frontend: Triggering Turnstile on Form Render

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>
  );
}

2. Backend: Validating the Token

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);
    }
  }
}

3. API Route Handler

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!" });
  });
}

4. Updating Content Security Policy (CSP)

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;

Security Requirements & Best Practices

To ensure your application is fully protected, keep these operational and security principles in mind:

  • Server-Side Validation is Mandatory: Generating a token on the frontend is not enough. You must validate the token against Cloudflare's /siteverify API to confirm it has not been tampered with. Without this step, your implementation is incomplete.
  • Tokens Expire Quickly: A Turnstile token is valid for exactly 300 seconds (5 minutes). Furthermore, each token can only be validated once. If a token has expired or already been redeemed, generate a fresh challenge before retrying the form submission.
  • Protect and Rotate Keys: Never expose your Secret Key in client-side code. If your backend ever accidentally logs or exposes it, immediately rotate your keys using the Cloudflare Dashboard or API.
  • Restrict Hostnames: Configure your Turnstile widget to strictly allow verifications originating only from domains you control to prevent abuse matching.
  • Separate Environments: Use separate Turnstile widgets for development, staging, and production. Name them descriptively like "Contact Form - Production" or "Login - Staging".

Testing Your Implementation

Before deploying to production, ensure you aren't disrupting actual user flows. Turnstile provides special "dummy" testing keys that behave predictably:

  • Site keys: Cloudflare offers designated testing Site Keys (such as 1x00000000000000000000AA) which will always successfully generate a token or force an interactive challenge for design testing, without actually skewing your Turnstile analytics.
  • Secret keys: You can pair the dummy Site Keys with designated testing Secret Keys on your backend (like 1x0000000000000000000000000000000AA which always passes, or 2x0000000000000000000000000000000AA which always fails). Note that your production Secret Key will reject test tokens.

Looking to Migrate?

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.

Adding Turnstile Privacy Addendum

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.

Frequently Asked Questions

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.

Share This Article

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

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

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 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

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.

© 2026 David Muraya. All rights reserved.