Every contact form on the internet is a tiny public mail relay, and the bots found yours a long time ago. The result is a familiar morning: three real inquiries a month buried under fifty pitches for SEO services, crypto, and "your website has errors."
The instinctive fix — slapping a puzzle CAPTCHA on the form — solves the bot problem by taxing every real visitor instead. This guide covers the layered approach that keeps the form invisible to humans and hostile to bots: honeypot, managed challenge, and a server-side check that treats the widget's token as a claim to verify, not a fact to trust. The same pattern runs this site's own contact form.
Why forms get spammed — and why puzzles are a poor answer
Form spam is industrial: bots crawl the web for <form> tags, fill every field, and submit. They are not attacking you specifically — you are a row in a scrape list. That matters for defense, because most submissions come from the dumbest possible bots, and dumb bots are cheap to defeat.
The trap is that the historically dominant fix, Google's reCAPTCHA, addresses a verification problem — "is this a human?" — when most sites actually have a triage problem: "is this submission worth delivering?" Verification tools force every visitor to prove humanity; triage tools quietly discard the garbage. Keep both ideas in the toolbox, but lead with triage.
The defenses, compared
| Defense | How it works | Visitor friction | Cost |
|---|---|---|---|
| Honeypot field | Hidden input (website, company_url) that bots fill and humans never see |
None — invisible | Free |
| Time trap | Reject submissions faster than a human can type | None | Free |
| Turnstile (managed) | Cloudflare's non-interactive challenge: proof-of-work, browser signals, no puzzle | Near zero; checkbox only for suspicious visitors | Free |
| reCAPTCHA v2 | Checkbox + image puzzles for flagged users | Highest — real puzzles | Free tier ~1M assessments/mo |
| reCAPTCHA v3 | Score 0.0-1.0 from behavioral signals; you set the threshold | Invisible, but silently fails open when over quota | Free tier ~1M/mo; Enterprise from 10k free |
| Content filter (Akismet etc.) | Scores message text for spam patterns | None | Paid tiers for commercial use |
| Rate limiting | Cap submissions per IP/session | None until abused | Free |
Prices and quotas are as of 2026 per the vendors' own documentation — reCAPTCHA's FAQ and Turnstile's docs.
Why reCAPTCHA stopped being the default
Nothing about reCAPTCHA is broken — it remains the largest bot-signal network on the web. But three shifts moved it out of the default slot for small sites:
- Friction moved wrong. v2 still shows image puzzles to exactly the visitors you most want to hear from — the hesitant ones on mobile. Every puzzle is a measurable abandonment point on a form that exists to generate inquiries.
- Privacy posture got heavier. reCAPTCHA sets the
_GRECAPTCHAcookie and sends behavioral signals to Google for risk analysis — which means consent-banner and privacy-policy obligations in EU-facing setups. Turnstile's documentation states it processes only the data needed for the security function and does not touch form contents — a materially lighter disclosure burden. - Quota behavior is sharp-edged. Per Google's own FAQ, v3 keys that exceed the free monthly quota fail open — returning a passing score with an error string, silently, for the rest of the month. The worst failure mode is the one you cannot see.
The honest case for reCAPTCHA: if your site already lives in Google's ecosystem, its signal depth is unmatched, and v3's invisibility is genuinely good UX — when under quota. Choose it knowingly, not by default.
The layered setup that works
This is the arrangement this site uses, and it is the right shape for any PHP or CMS form:
- Honeypot first. Add a real-looking field —
name="website"— hidden with CSS (nevertype="hidden"; smarter bots skip those). When it arrives filled, return a fake success so the bot moves on. Never return an error that teaches it to retry. - Turnstile managed widget on the form. The widget types matter: Managed shows a checkbox only to suspicious visitors, Non-interactive never does. Embed the script, put the widget div inside the form, done client-side.
- Verify server-side, always. The widget emits a
cf-turnstile-responsetoken. Your endpoint POSTs it tohttps://challenges.cloudflare.com/turnstile/v0/siteverifywith your secret:
$payload = http_build_query([
'secret' => $secret, // from config outside the web root
'response' => $_POST['cf-turnstile-response'] ?? '',
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
]);
$ctx = stream_context_create(['http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => $payload,
'timeout' => 10,
]]);
$verify = json_decode(@file_get_contents(
'https://challenges.cloudflare.com/turnstile/v0/siteverify', false, $ctx), true);
- Check the full response, not just
success. Also compareactionagainst your form name andhostnameagainst your domain — a valid token minted for another site is still valid. This site's endpoint does exactly that before callingmail(). - Keep the secret out of the web root. Ours lives in a
config.phpone directory up, written by CI — never in the repository and never indist/. - Rate-limit and log sensibly. Cap submissions per IP, and log a hash of the IP, not the raw address — you get abuse tracking without building a store of personal data.
- Test the failure path. Submit with no token and confirm a 403-style rejection, not a 500. A form that errors loudly on missing verification is telling you it would fail open under stress.
What still gets through
Layers stop bots; they do not stop people. Human spam farms and determined manual pitches will always produce a trickle — typically a few messages a month on a small site. The answer there is boring: a content filter if volume justifies one, a faster delete key if it doesn't. What you should never do in response is raise the challenge difficulty — that punishes your real prospects for someone else's abuse.
Also worth remembering: spam defenses sit on top of basic site hygiene, not instead of it. If the CMS underneath is out of support, a perfect form is a locked door on an open house — our end-of-life check guide covers that side. Browse the full guides index for the rest of the maintenance stack.
— Editorial team. Facts current as of 2026; we revise guides when the ground shifts.
Frequently Asked Questions
Do I need Turnstile if I already have a honeypot?
For a low-traffic site, a honeypot alone removes most automated submissions — try it first and watch the log. Add Turnstile when spam resumes, because better bots render JavaScript and skip hidden fields.
Is Cloudflare Turnstile really free?
Yes — Turnstile is free and does not require putting your site on Cloudflare's CDN, per its own documentation. You create a site key and secret in a free Cloudflare account and verify tokens server-side.
What about spam sent by humans, not bots?
No challenge stops a person pasting a sales pitch. Rate-limit submissions per IP, and if volume justifies it, route messages through a content filter like Akismet. Expect a trickle of manual spam regardless — it is the cost of an open form.