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

· 7 min read · 15 views · Getwebup

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 ones we see most often on customer sites.

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 — this is the single most common cause we see in support tickets.

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;
  3. 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.
  4. 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
    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 support to confirm outbound 443 isn't restricted.
  3. 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.
  4. 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 addWhy
yourdomain.comRoot domain
www.yourdomain.comwww redirect target, if visitors land here first
staging.yourdomain.comStaging/testing environment
localhost / 127.0.0.1Local 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.

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.

Questions people actually ask