# Cloudflare Turnstile: stop bots without annoying CAPTCHAs

> How to protect a form from bots with Cloudflare Turnstile. Add the widget to your page and verify the token from a Worker.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-07-02 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/cloudflare-turnstile/

The moment you put a public form online, bots find it. Sign-up forms, contact forms, comment boxes. You need a way to tell humans from scripts.

The old answer was a CAPTCHA: squint at blurry traffic lights. **Turnstile** is Cloudflare's replacement. Most of the time the user does nothing at all, it just checks a box for them in the background.

It works in two halves: a widget on your page, and a check on your server. Let me show both.

## Get your keys

In the Cloudflare dashboard, you create a Turnstile widget and get two keys:

- a **site key**, which is public and goes in your HTML
- a **secret key**, which stays on your server

For trying things out, Cloudflare has test keys that always pass. The site key `1x00000000000000000000AA` and the secret `1x0000000000000000000000000000000AA` are handy while you build.

## Add the widget

On your page, load the Turnstile script and drop in a div with your site key:

```html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form method="POST" action="/submit">
  <input type="email" name="email" required />
  <div class="cf-turnstile" data-sitekey="your-site-key"></div>
  <button type="submit">Sign up</button>
</form>
```

That's the whole frontend. The widget runs, and when it passes, it adds a hidden field called `cf-turnstile-response` to your form. That field holds a token.

## Verify on the server

A token from the page isn't proof on its own. A bot could fake the form post. So your Worker must ask Cloudflare "is this token real?" using your secret key.

```js
export default {
  async fetch(request, env) {
    const form = await request.formData()
    const token = form.get('cf-turnstile-response')

    const result = await fetch(
      'https://challenges.cloudflare.com/turnstile/v0/siteverify',
      {
        method: 'POST',
        body: new URLSearchParams({
          secret: env.TURNSTILE_SECRET_KEY,
          response: token,
        }),
      }
    )

    const outcome = await result.json()

    if (!outcome.success) {
      return new Response('Failed the bot check', { status: 403 })
    }

    // The user is human, handle the form
    return new Response('Thanks!')
  },
}
```

The flow is: read the token from the form, POST it to the `siteverify` endpoint with your secret, and check `outcome.success`. Only then do you trust the submission.

Keep the secret key as a Cloudflare secret, not in your code:

```bash
npx wrangler secret put TURNSTILE_SECRET_KEY
```

## Why I like it

Turnstile is free, it's privacy-friendly, and for real users it's usually invisible. No puzzles, no friction.

The one rule to never break: **always verify the token on the server.** The widget on its own can be bypassed. The server check is what actually stops the bots.

The full reference is in the [Turnstile docs](https://developers.cloudflare.com/turnstile/).
