Contact form spam is one of those problems that looks small until it starts costing you. Sales gets a dozen junk inquiries a day, a real lead gets buried, and someone eventually suggests adding a puzzle that makes every visitor click traffic lights. Cloudflare Turnstile is a better answer. It is a free CAPTCHA alternative that verifies visitors in the background, and for most people it never shows anything at all.

On its own, though, Turnstile is one layer. The contact form on our own site uses Turnstile plus four cheap checks, and together they stop the junk without turning away the people we actually want to hear from. It caught a Russian-language spam message on its first day live. This article walks through how Turnstile works, the settings we chose, and the extra layers that make it hold up.

What Cloudflare Turnstile actually does

Turnstile runs a set of small, non-intrusive challenges in the visitor's browser and decides whether the visitor looks like a person or a script. When it is satisfied, it issues a token. Your form sends that token along with the submission, and your server asks Cloudflare whether the token is valid before doing anything with the message.

You do not need to host your site on Cloudflare to use it. Turnstile works on any website. You create a widget in the Cloudflare dashboard, which gives you a site key (public, goes in your page) and a secret key (private, stays on your server).

Widget modes: managed, non-interactive and invisible

Cloudflare offers three widget modes, and the choice matters more than most people realize.

Mode What the visitor experiences
Managed Cloudflare decides based on risk. Most visitors pass without interacting; a checkbox appears only when more verification is needed.
Non-interactive A visible widget with a loading spinner. The visitor is never asked to do anything.
Invisible Nothing visible at all. Challenges run entirely in the background.

Cloudflare recommends managed mode, and so do we. It keeps the option of asking a suspicious visitor to click a checkbox, which is a real deterrent to bots, while letting ordinary visitors through untouched.

Appearance: keep the widget out of sight

Separately from the mode, managed and non-interactive widgets have an appearance setting:

  • always: the widget is visible from page load (the default)
  • execute: the widget appears only once the challenge starts, such as when someone begins filling out the form or presses submit
  • interaction-only: the widget appears only if the visitor actually needs to interact

We run managed mode with interaction-only appearance. In practice, a real prospect filling out our form sees no widget at all. Only a visitor that Cloudflare is unsure about gets a checkbox. That is the balance you want on a lead form: zero friction for buyers, a speed bump for anything that looks automated.

The client side is a script tag and a div:

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

<form method="POST" action="/api/contact">
  <!-- your fields -->
  <div class="cf-turnstile"
       data-sitekey="YOUR_SITE_KEY"
       data-appearance="interaction-only"
       data-action="contact"></div>
  <button type="submit">Send</button>
</form>

Turnstile adds a hidden field named cf-turnstile-response to the form containing the token. The data-action value is optional, but it lets you confirm on the server that the token came from this specific form.

Server-side validation is not optional

This is where most quick installs go wrong. Putting the widget on the page does nothing by itself. Cloudflare's documentation is explicit that the client-side widget alone does not protect your form, because a script can post directly to your endpoint with a made-up token. You have to validate every token on your server.

Validation is a single POST to Cloudflare's siteverify endpoint:

POST https://challenges.cloudflare.com/turnstile/v0/siteverify
secret=<your secret key>
response=<the cf-turnstile-response token>
remoteip=<visitor IP, optional>

A few rules to build around, all from Cloudflare's documentation:

  • Tokens are valid for 300 seconds (five minutes). A visitor who leaves the form open for ten minutes will need a fresh token, so handle expiry gracefully rather than showing a cryptic error.
  • Each token can be validated only once. A replayed token fails with the error code timeout-or-duplicate.
  • Check the hostname and action in the response. If you set data-action="contact", confirm the response says contact, and confirm the hostname is your domain.
  • Keep the secret key on the server. Never put it in front-end code.

If siteverify says the token is not valid, reject or flag the submission. Do not quietly accept it.

The other four layers we use

Turnstile is very good at stopping automated browsers and scripts. It is not designed to judge whether a message is junk, and a determined spammer can pay humans to pass challenges. That is why we stack a few simple checks behind it. Each one is cheap to build and catches a different kind of abuse.

1. A honeypot field

Add a text field that real people never see, hidden with CSS and labeled with something tempting like "website" or "company_url". Humans leave it empty. Many bots fill in every field they find. If it has a value, flag the submission. Make sure the field is hidden from screen readers too (aria-hidden and tabindex="-1") so you do not trip up visitors using assistive technology.

2. A timing check

Record when the page loaded, in a hidden field or a signed value, and compare it to when the form was submitted. Nobody types a name, email and thoughtful message in under three seconds. Our form flags anything submitted faster than that. It is a crude test, and that is fine. It costs nothing and quietly catches a lot of scripted traffic.

3. A rate limit

Limit how many submissions a single IP address can make in a window. Ours allows five per IP per hour. A real prospect might submit twice if they forget something. Nobody legitimate submits twenty times. On Cloudflare you can enforce this in the Worker that handles the form, or with a rate limiting rule in front of the endpoint.

4. A deliberately lenient content filter

This is the layer people tend to overdo. It is tempting to block messages containing links, certain words or non-English text. The problem is that real prospects send links all the time: their website, a listing they want you to look at, a deck in a shared folder. Block links and you will lose real leads without ever knowing.

Our filter only flags two patterns: HTML or BBCode link markup (the [url=...] and style that spam tools use and people almost never type into a form), and messages with four or more links. Everything else passes. We would rather review an occasional borderline message than miss a buyer.

Flag, store, but do not email

The final design choice is what to do with a submission that fails a check. We do not throw it away and we do not show the sender an error. Flagged submissions are saved to the database with the reason they were flagged, and they are simply not emailed to the team.

That gives you three benefits:

  1. Nothing legitimate is lost. If a real person trips a check, the message is still there to review.
  2. The spammer gets no signal. The form responds normally, so there is nothing to probe or tune against.
  3. You can tune your rules from evidence. After a few weeks you can see exactly which checks fire and whether any of them are catching real people.

How we built it on Cloudflare

For reference, here is the shape of the contact form on our own site, which we moved to Cloudflare along with the rest of the site:

  • The form posts to a Cloudflare Worker.
  • The Worker validates the Turnstile token with siteverify, then runs the honeypot, timing, rate limit and content checks.
  • Every submission, clean or flagged, is written to a Cloudflare D1 database.
  • Clean submissions are emailed to the team through Cloudflare Email Routing, sent from a dedicated forms subdomain so form mail never affects the reputation of our main mail domain.
  • The email is sent in the background after the Worker has already responded, so the visitor gets an instant confirmation instead of waiting on mail delivery.

If you want help putting this in front of your own forms, or moving your site to Cloudflare at the same time, that is part of our website hosting and migration work. If you are planning a broader move, our website migration checklist covers the rest.

Common mistakes to avoid

  • Skipping siteverify. The widget renders, the form works, and bots post straight to the endpoint. Always validate on the server.
  • Using the same secret key everywhere. Create a separate widget per site so a leaked key has a limited blast radius.
  • Showing errors to flagged submissions. It tells the spammer exactly which check to work around.
  • Over-filtering content. Blocking links or foreign-language text loses real leads.
  • Forgetting accessibility. Hidden honeypots should be hidden from assistive technology as well as from sight.

Frequently asked questions

Is Cloudflare Turnstile free?

Turnstile has a free tier that covers most business websites, and Cloudflare offers an enterprise version with additional features. Plans and limits change, so check Cloudflare's current Turnstile pricing page before you rely on a specific limit.

Do I need to host my site on Cloudflare to use Turnstile?

No. Turnstile works on any website. You add the script and widget to your form and call the siteverify endpoint from whatever server handles submissions. Hosting on Cloudflare just makes it convenient to run the validation in a Worker.

Will Turnstile hurt my conversion rate?

With managed mode and interaction-only appearance, most visitors never see the widget, so the effect on real people is minimal. The bigger risk to conversion is usually an aggressive content filter, not Turnstile itself.

Why do I still get some spam after adding Turnstile?

Turnstile is built to tell people from automated traffic. Spam typed by real people, or submitted through paid challenge-solving services, can still pass. That is why we pair it with a honeypot, a timing check, a rate limit and a light content filter, and store flagged messages for review.

What happens if a visitor's token expires?

Tokens last five minutes and can be used once. If someone leaves the form open and submits later, siteverify will reject the token. Handle this by resetting the widget to get a fresh token and asking the visitor to submit again, rather than discarding what they typed.