If your website has a contact form, it gets spam. Usually it starts a few weeks after launch: a trickle of SEO offers, crypto pitches, and messages in languages your business doesn’t operate in. Then it becomes a flood, and the real inquiries, the ones that pay your bills, get buried or end up in your spam folder alongside the junk.
The instinctive fix is to bolt on a CAPTCHA: “click every image with a traffic light.” That stops some bots, but it also stops some customers. Every extra step on a contact form costs you inquiries, and image puzzles are especially hard for people using assistive technology.
The better approach is layers: several cheap, invisible checks that each catch a different kind of spam. This is how we protect the forms on our own site and on client sites.
Understand Who’s Filling In Your Form
Spam comes from three broad sources, and each needs a different defense:
- Dumb bots. Scripts that crawl the web, find any
<form>, fill every field with junk, and submit. This is most of the volume. - Smarter bots. Scripts that run a real browser engine, execute JavaScript, and can sometimes get past simple checks. Less common, more persistent.
- Humans paid to spam. People pasting messages into forms by hand. No technical check fully stops a real person, but rate limits and content rules reduce the damage.
No single technique handles all three, which is why you layer them.
Layer 1: The Honeypot Field
A honeypot is a form field that real visitors never see, but bots do. Because simple bots fill in every input they find, any submission with something in the honeypot is almost certainly automated.
<div class="hp" aria-hidden="true">
<label for="website_url">Leave this field empty</label>
<input type="text" id="website_url" name="website_url"
tabindex="-1" autocomplete="off">
</div>
.hp { position: absolute; left: -9999px; }
The details matter:
- Don’t use
type="hidden". Bots know to skip hidden inputs. Use a normal text field and hide it with CSS. - Give it a believable name.
website_urlorcompany_sitegets filled in;honeypotdoes not. - Use
tabindex="-1"andautocomplete="off"so keyboard users don’t tab into it and browsers don’t autofill it. - Use
aria-hidden="true"so screen readers skip it. The label is a fallback in case they don’t. - Check it on the server. If the field has any value, reject the submission. Return a normal “thanks” response so the bot doesn’t learn it was caught.
A honeypot costs nothing, adds no friction, and on a typical small business site stops a large share of automated spam on its own.
Layer 2: An Invisible Challenge
For the bots that get past a honeypot, add a challenge. Modern challenges like Cloudflare Turnstile usually don’t ask the visitor to do anything. They run a set of background checks in the browser and only show an interactive prompt (typically a single checkbox) when something looks suspicious.
Turnstile is free and works on any site, not just ones hosted on Cloudflare. The setup has two halves, and the second one is the one people forget.
The browser half
You create a widget in the Cloudflare dashboard, which gives you a public site key and a private secret key. You add Cloudflare’s script and a placeholder <div> with your site key to the form. When the check passes, the widget adds a hidden field containing a token to the form.
The server half (the important part)
That token proves nothing on its own until your server sends it to Cloudflare’s verification endpoint along with your secret key and asks “is this token real?”:
POST https://challenges.cloudflare.com/turnstile/v0/siteverify
secret=<your secret key>
response=<the token from the form>
Only accept the submission if the response says "success": true.
If you skip this step, the widget is purely decorative: a bot can submit the form without a token, or with a made-up one, and your server will accept it. We’ve seen this in audits more than once. The widget is on the page, the site owner believes they’re protected, and nothing checks the token.
Two more things to know: tokens expire after a few minutes and are single-use. If a visitor takes a long time to write a message, the widget needs to refresh the token before submission (Turnstile can do this automatically), and your server should reject a token it has already seen.
Pro Tip
Never put a secret key in front-end code. The site key is public by design; the secret key must only ever live on your server or in a serverless function. If a secret key has been in your HTML or JavaScript, rotate it in the dashboard right away.
Layer 3: Server-Side Sanity Checks
Your server should treat every submission as untrusted, because it is. A bot doesn’t have to use your form at all; it can send a request straight to whatever URL your form posts to. Any check that runs only in the browser can be skipped.
Cheap checks that catch a surprising amount:
- Required fields are present and the right length. A “name” field with 4,000 characters isn’t a name.
- The email address is plausibly formatted. Don’t over-engineer it; just reject obvious garbage.
- Too fast to be human. Record when the form was rendered (in a hidden field or a signed token) and reject submissions that arrive within a couple of seconds. People don’t fill in a contact form in 400 milliseconds.
- Link count. A first message from a new customer rarely contains five URLs. Flag or reject messages above a small threshold.
- No header injection. If any field ends up in an email header (like a “reply-to”), strip newline characters. Otherwise an attacker can use your form to send email to anyone.
Layer 4: Rate Limiting
Rate limiting caps how many submissions a single source can make in a time window: for example, five per IP address per ten minutes. It won’t stop a single spam message, but it stops one script from sending you 3,000 of them overnight, and it protects whatever runs behind your form (your email quota, your CRM, your database).
One implementation detail trips people up on modern hosting. If your form handler runs as a serverless function, a simple in-memory counter doesn’t work. Each request may land on a different instance that starts with an empty counter. You need a shared store: your host’s built-in rate-limiting feature, a key-value store, or your database. We learned this one ourselves. An early version of a rate limiter in one of our services looked correct in testing and did almost nothing in production for exactly this reason.
Layer 5: Watch What Gets Through
Whatever’s left will be mostly real messages, but check periodically:
- Keep caught spam in a separate folder or table rather than deleting it, so you can spot false positives.
- If a pattern keeps getting through, such as the same phrase or the same sending domain, add a specific rule for it.
- Make sure legitimate notification emails aren’t landing in your own spam folder. Set up SPF, DKIM, and DMARC for the domain that sends them.
What We Don’t Recommend
- Image CAPTCHAs as the first line of defense. They add friction for everyone to stop a minority of bots.
- Publishing a bare email address instead of a form. It just moves the spam to your inbox, where you have fewer tools to filter it.
- Blocking entire countries unless you’re certain you’ll never get legitimate business from them. It’s blunt and easy to get wrong.
Putting It Together
A well-protected contact form looks completely ordinary to a customer: a few fields and a submit button. Behind it:
- A honeypot catches the crude bots.
- An invisible challenge, verified on the server, catches most of the rest.
- Server-side checks reject malformed and suspiciously fast submissions.
- A shared rate limit stops floods.
- You review what gets through and tune the rules.
Each layer is simple. Together they turn a spam-flooded inbox into one you can trust again, without making a single real customer prove they’re human.
❓ Frequently Asked Questions
Is a honeypot field enough on its own?
For a small site, a honeypot alone often stops the majority of automated spam, because most bots fill in every field they find. It won’t stop bots built for your specific site or form-filling services that use real browsers, so pair it with a challenge and server-side checks.
Does Cloudflare Turnstile require my site to be on Cloudflare?
No. Turnstile works on any website. You create a widget in the Cloudflare dashboard to get a site key and a secret key, add the widget script to your form, and verify the token from your server.
Will spam protection hurt accessibility?
Image puzzles do. They’re hard for people using screen readers and for people with low vision. Honeypots, invisible challenges, and server-side checks don’t add any task for the visitor, which is one of the main reasons to prefer them.
Drowning in Form Spam?
Every form we build ships with these layers already in place, verified on the server and rate-limited.
Was this article helpful?
WebGlo