DEV Community

Getwebup
Getwebup

Posted on Edited on Originally published at getwebup.in Fully Autonomous

Cloudflare Turnstile Not Working in WordPress? Here's the Fix

You swapped out reCAPTCHA for Cloudflare Turnstile because it's faster, free, and doesn't track your visitors — and now the widget won't load, or it loads but every form submission comes back "verification failed." Here's how to actually fix it, not just refresh the page and hope.

Why site owners are moving to Turnstile

Google reCAPTCHA works, but it ships third-party tracking cookies, occasionally shows an infinite loop of traffic-light photos to real customers, and adds a noticeable delay before your contact form is usable. Cloudflare Turnstile solves the same problem — tell bots from humans — without the cookie banner headache or the "select all the crosswalks" tax on your visitors. Most of the time it runs an invisible, non-interactive check and just passes.

The catch: because it verifies the visitor's browser and the exact domain it's embedded on, a handful of setup details will silently break it. Below are the most common ones.

Symptom 1: The widget never appears

You add the shortcode or plugin block, save the page, and where the checkbox or Cloudflare badge should be, there's just... nothing. No error in the form, no console warning you'd notice at a glance.

Cause

  • The Turnstile JavaScript (https://challenges.cloudflare.com/turnstile/v0/api.js) is being blocked by a caching or "defer non-essential JS" plugin like WP Rocket, LiteSpeed Cache, or Autoptimize.
  • A Content Security Policy header (set in .htaccess or a security plugin) doesn't allow scripts from challenges.cloudflare.com.
  • The site key was pasted with a stray space or line break.

Fix

  1. Open your browser's DevTools console (F12) on the page with the form. If you see a CSP violation error naming challenges.cloudflare.com, that confirms cause #2.
  2. If you're using a CSP header, add this to your script-src and frame-src directives:
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
Enter fullscreen mode Exit fullscreen mode
  1. Exclude the Turnstile script from JS defer/delay optimisation. In WP Rocket: Settings → File Optimization → Excluded JavaScript Files, add challenges.cloudflare.com. In LiteSpeed Cache: Page Optimization → JS Settings → JS Excludes.
  2. Re-copy your site key from the Cloudflare dashboard (Turnstile → your widget → Sitekey) and paste it into a plain-text editor first to strip formatting, then into your plugin settings.

Symptom 2: Widget shows, but every submission fails with "verification failed"

The checkbox or badge renders fine. The visitor completes it (or it auto-passes). They click submit, and the form bounces back with a generic error, or the WordPress error log shows something like cf-turnstile-response missing or invalid-input-response.

Cause

This is almost always a mismatch between the site key (public, goes in your HTML) and the secret key (private, used server-side to verify the token with Cloudflare's API). Common ways this happens:

  • You pasted the secret key into the site key field, or vice versa — they look similar and it's an easy copy-paste mistake.
  • You have a staging and a production Turnstile widget, and the keys got swapped during deployment.
  • Your server can't reach Cloudflare's verification endpoint (https://challenges.cloudflare.com/turnstile/v0/siteverify) — outbound HTTPS is blocked by a firewall rule, mod_security, or an aggressive VPS security group.
  • A caching plugin is caching the form page including the one-time Turnstile token, so the visitor submits a token that already expired (tokens are valid for roughly 5 minutes and are single-use).

Fix

  1. In the Cloudflare dashboard, go to Turnstile and confirm which key is which — the sitekey is the one you never treat as secret. Re-check both fields in your plugin.
  2. Test outbound connectivity from your server directly:
curl -I https://challenges.cloudflare.com/turnstile/v0/siteverify
Enter fullscreen mode Exit fullscreen mode

If this times out or connection-refuses, your hosting firewall (CSF, ModSecurity, or a VPS security group) is blocking outbound HTTPS to that host. On a cPanel/CSF server, whitelist the outbound connection in /etc/csf/csf.conf or ask your host to confirm outbound 443 isn't restricted.

  1. Exclude the page containing your form from full-page caching, or at minimum exclude any URL parameter/cookie the form plugin sets. Most contact-form plugins (Contact Form 7, WPForms, Gravity Forms) already send no-cache headers on their own AJAX endpoint — check that your caching plugin isn't overriding that.
  2. If you run WooCommerce checkout or a login form behind Turnstile, make sure the widget re-renders after AJAX page updates (cart totals refresh, etc.) — a stale token from before the AJAX reload will always fail.

Symptom 3: Works on staging, fails on the live domain

You tested everything on staging.yoursite.com, it passed every time. You push to production and it breaks immediately.

Cause

Turnstile widgets are locked to the exact hostnames you list when you create them in the Cloudflare dashboard. If your production domain, its www variant, or your staging subdomain isn't in that list, verification fails with a domain mismatch — Cloudflare's API rejects it silently from the form's point of view.

Fix

In Cloudflare → Turnstile → your widget → Settings, check the "Domains" list. Add every hostname the form is actually served from:

Hostname you need to add Why
yourdomain.com Root domain
www.yourdomain.com www redirect target, if visitors land here first
staging.yourdomain.com Staging/testing environment
localhost / 127.0.0.1 Local development — Turnstile has a dedicated toggle for this

Changes to the domain list apply within a minute or two — no need to regenerate the keys.

Symptom 4: Turnstile passes, but the form still submits nothing (WPForms/CF7)

This one isn't a Turnstile problem at all — it's a plugin integration gap. Some older versions of popular form plugins added Turnstile support after reCAPTCHA, and the field name they expect doesn't match what a hand-rolled integration sends.

Fix

  • Update the form plugin to the latest version — Turnstile support in Contact Form 7, WPForms, and Gravity Forms has matured a lot since first release and early versions had known bugs here.
  • If you added Turnstile manually via a snippet, confirm the hidden field name is exactly cf-turnstile-response — this is what the server-side verification call reads.
  • Check wp-content/debug.log (with WP_DEBUG_LOG enabled) for a PHP notice about an undefined index — that usually points straight at the mismatched field name.

Symptom 5: "Invalid request: missing Turnstile response"

This is a different failure from "verification failed." When the error says the Turnstile response is missing, no token reached your server at all — the cf-turnstile-response field was absent or empty in the submission. Cloudflare's own verification API reports the same condition as missing-input-response: "Response parameter was not provided." Your keys can be perfectly correct and this will still fail, because the problem is in the browser or the form.

Cause

  • The widget sits outside the <form> it's meant to protect — in a popup, a separate page-builder block, or the footer. Turnstile's hidden cf-turnstile-response field is only sent with a form when the widget is embedded inside that <form> element; outside it, the challenge passes and the form still submits without a token.
  • The form submits over AJAX, and the JavaScript that builds the request never adds the token. With explicit rendering you have to read it yourself with turnstile.getResponse(widgetId) and include it.
  • The visitor pressed submit before the widget had finished loading, because the Turnstile script is being delayed or deferred by an optimisation plugin (see Symptom 1).

Fix

  1. Open DevTools → Network, submit the form, and look at the request payload. No cf-turnstile-response field, or an empty one, confirms it: the token never left the browser.
  2. Move the widget's container inside the <form> element it protects.
  3. For AJAX or custom forms, get the token with turnstile.getResponse(widgetId) and send it as cf-turnstile-response alongside the other fields.
  4. Exclude challenges.cloudflare.com from JS delay and defer, so the widget has rendered before anyone can submit.

Turnstile error messages, decoded

Most people land here with an exact error string. Each one below points to a different link in the chain: the browser never produced a token, the token never reached your server, or your server sent it to Cloudflare and was told no. Find your message, then jump to the matching fix above.

Error message What it means Fix
"Turnstile verification failed", turnstile_verification_failed, "Incorrect Turnstile response. Try the verification again." A token reached your server, your server sent it to Cloudflare's siteverify API, and Cloudflare answered "success": false. Symptom 2 — a key mismatch, blocked outbound HTTPS, or an expired or reused token.
"Invalid Turnstile response", invalid-input-response Cloudflare received a token, but it isn't valid: malformed, expired, or issued by a different widget from the one your secret key belongs to. Symptom 2, step 1 — check that the site key and the secret key come from the same widget.
timeout-or-duplicate The token is older than 300 seconds, or it has already been checked once. Every token is single-use. Reset the widget after a failed submit with turnstile.reset(widgetId), and make sure your code verifies each token only once.
"Turnstile token missing", "Incorrect or missing Turnstile token", "The cf-turnstile-response field is required.", "Invalid request: missing Turnstile response", missing-input-response No token arrived with the form at all. The problem is in the browser or the form, not your keys. Symptom 5.
missing-input-secret, invalid-input-secret The secret key in your plugin or code is empty or wrong. Symptom 2, step 1 — copy the secret key from the Cloudflare dashboard again.
"Something went wrong with Turnstile", "Turnstile not initialized" The widget failed in the browser before it could produce a token: its script was blocked or delayed, or the page's domain isn't on the widget's hostname list (Cloudflare reports that one as client-side error 110200). Symptoms 1 and 3.
"Siteverify isn't being called for this widget" (a notice in the Cloudflare dashboard) Visitors' browsers are getting tokens, but your server never checks them with siteverify. The widget is on the page, but nothing enforces it, so a bot can simply skip it. Turn on Turnstile validation for that form in your form plugin, not just the widget. In custom code, POST the token and your secret key to siteverify and reject the submission unless success is true.

What does "Turnstile verification failed" actually mean?

Turnstile decides whether a visitor looks like a bot in the browser, before a token exists. By the time your form says "verification failed", the browser has produced a token and your server has asked Cloudflare to confirm it. So the message almost never means "this visitor is a bot". It means the confirmation step broke: the wrong secret key, a token used twice or held for more than five minutes, or a server that can't reach challenges.cloudflare.com.

Prevention checklist

  • Keep a copy of your site key and secret key in a password manager, labelled clearly — don't rely on remembering which is which.
  • Add every environment's hostname to the widget's domain list before you deploy, not after something breaks.
  • Exclude the Turnstile script and your form page from aggressive JS-delay and full-page caching from day one.
  • After any migration (new server, new domain, cPanel-to-cPanel move), re-verify the domain list and re-test the form once DNS has cut over.
  • If you self-host behind a VPS firewall, confirm outbound HTTPS to challenges.cloudflare.com is allowed as part of your first-hour server checklist.

FAQ

Is Cloudflare Turnstile free to use?

Yes. Turnstile is free for unlimited sites and requests — you don't need to be on Cloudflare's paid plans or even proxy your DNS through Cloudflare to use it. You only need a free Cloudflare account to generate a site key and secret key.

Do I need my domain's nameservers on Cloudflare to use Turnstile?

No. Turnstile is a standalone product — it works on any domain, on any host, regardless of who manages your DNS.

Can I run Turnstile and reCAPTCHA on the same site during migration?

Most plugins let you configure only one CAPTCHA provider per form at a time, but you can run Turnstile on some forms and reCAPTCHA on others while you migrate. Just don't leave both scripts loading on the same form — they can conflict over the same submit button hook.

Why does the Turnstile checkbox stay stuck on 'verifying'?

This almost always means the browser can't reach challenges.cloudflare.com — check for an ad blocker or privacy extension blocking the request, a corporate firewall, or (server-side) an expired SSL certificate on your own domain that's breaking the page's mixed-content rules.

Does Turnstile slow down my page like reCAPTCHA sometimes does?

It's noticeably lighter — Turnstile's managed challenge is designed to run invisibly in most cases and only escalates to an interactive check for suspicious traffic, so the vast majority of real visitors never see more than a brief badge.

What does 'Turnstile verification failed' mean?

Your server sent the visitor's Turnstile token to Cloudflare to check, and Cloudflare rejected it. It is almost always a setup problem rather than a bot: the site key and secret key come from different widgets, the token was used twice or is older than five minutes, or your server can't reach challenges.cloudflare.com.

What is the cf-turnstile-response field?

It is the hidden field Turnstile adds inside your form to carry the token to your server. 'The cf-turnstile-response field is required' means the form arrived without it, usually because the widget sits outside the form, an AJAX form didn't include it, or an optimisation plugin delayed the Turnstile script.


Originally published on the Getwebup blog.

This article was created with the help of AI.

Top comments (0)