Layer cheap checks before annoying ones. Add a hidden honeypot field that people never see but bots fill in, reject forms submitted faster than a person could type, and if spam still gets through, add a CAPTCHA service such as Turnstile, reCAPTCHA or hCaptcha whose token your server verifies.
| Method | What visitors notice | Stops | Needs server code |
|---|---|---|---|
| Honeypot field | Nothing | Simple bots that fill every field | Yes, a one-line check |
| Time trap | Nothing | Bots that submit instantly | Yes |
| Cloudflare Turnstile | Usually nothing, sometimes a checkbox | Most automated posting | Yes, a siteverify call |
| reCAPTCHA v3 | Nothing; you get a score | Most automated posting | Yes, verify and pick a threshold |
| hCaptcha | Sometimes a puzzle | Most automated posting | Yes, a siteverify call |
Updated
Spam bots crawl the web for forms and post to them automatically. Your server cannot see who is typing, so every defence works by spotting behaviour a person would not show, or by asking a separate service to vouch for the visitor. Try the first two defences in the demo, then copy the server check that goes with them.
Nothing is sent anywhere. Pressing Send runs the same checks a server would, right here in your browser.
Your name
Message
Act like a bot: fill in the hidden field
Send
Fill in the form and press Send to see what the server check decides.
The HTML goes in your form. The server check is shown for Node with Express, but the logic is three lines in any language.
The .hp class moves the trap off screen. The started field is filled with the current time by the small script.
<form action="/contact" method="post">
<label>Name <input name="name" required></label>
<label>Message <textarea name="message" required></textarea></label>
<div class="hp" aria-hidden="true">
<label>Website <input name="website" tabindex="-1" autocomplete="off"></label>
</div>
<input type="hidden" name="started" id="started">
<button type="submit">Send</button>
</form>
<style>.hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}</style>
<script>document.getElementById("started").value = Date.now();</script>Bots get the same reply as people so they cannot learn which check caught them.
app.post("/contact", express.urlencoded({ extended: false }), (req, res) => {
const { website, started, name, message } = req.body;
const seconds = (Date.now() - Number(started)) / 1000;
const isBot = Boolean(website) || !Number.isFinite(seconds) || seconds < 3;
if (!isBot) sendEmail({ name, message }); // your own mail function
res.redirect("/thanks");
});All three work the same way. These details come from each provider's own documentation.
| Turnstile | reCAPTCHA v3 | hCaptcha | |
|---|---|---|---|
| Script | challenges.cloudflare.com/turnstile/v0/api.js | Loaded from Google with your site key | js.hcaptcha.com/1/api.js |
| Widget | <div class="cf-turnstile" data-sitekey> | No widget; call execute on submit | <div class="h-captcha" data-sitekey> |
| Verify at | challenges.cloudflare.com/turnstile/v0/siteverify | Google's verify endpoint | api.hcaptcha.com/siteverify |
| Token life | 300 seconds, single use | Two minutes | See provider docs |
| Result | success true or false | success plus a 0.0 to 1.0 score | success true or false |
The widget adds a field named cf-turnstile-response to your form. Keep the secret in an environment variable.
async function verifyTurnstile(token, ip) {
const body = new URLSearchParams({
secret: process.env.TURNSTILE_SECRET,
response: token,
remoteip: ip,
});
const r = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body });
const data = await r.json();
return data.success === true;
}
A honeypot and a time trap cost visitors nothing and catch the large share of spam that comes from crude scripts. A CAPTCHA adds a third-party script, a privacy notice entry and sometimes a challenge, so save it for when you need it.
Use CSS that moves the field off screen rather than type="hidden", because many bots skip hidden inputs. Turn off autofill on it with autocomplete="off", since a browser that fills it in would flag a real person as a bot.
MDN notes that browsers may ignore autocomplete="off" for login fields, so do not name the trap email or password.
Each service puts a token into the form. Your server posts that token with your secret key to the provider's siteverify address and continues only if the reply says success is true.
Turnstile tokens are valid for 300 seconds and can be checked once. reCAPTCHA v3 tokens last two minutes, so request one when the visitor submits, not on page load.
An input that is in the form's code but hidden from people. Visitors leave it empty because they never see it; bots that fill every field they find put something in it, which gives them away.
Yes. Cloudflare Turnstile and hCaptcha both offer free tiers with the same shape: a script and a widget on the page, then a server call to a siteverify address with your secret key and the visitor's token. Check each provider's current plan page for limits.
No. Google's docs say v3 returns a score where 1.0 is very likely a good interaction and 0.0 is very likely a bot, and suggest starting with a threshold of 0.5. You decide what to do with low scores.
No. Cloudflare's documentation states plainly that the client-side widget alone does not protect your forms. A bot can post straight to your form address, so the server must verify the token.
Some bots run a real browser and only fill visible fields, and some spam is typed by people. Add the time trap, then a CAPTCHA. Also reject messages full of links if your form never needs them.
Related: How to add a contact form, Send a form to email, Spam word checker, On-page SEO checklist.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.