How to Stop Contact Form Spam

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.

Spam defences compared
MethodWhat visitors noticeStopsNeeds server code
Honeypot fieldNothingSimple bots that fill every fieldYes, a one-line check
Time trapNothingBots that submit instantlyYes
Cloudflare TurnstileUsually nothing, sometimes a checkboxMost automated postingYes, a siteverify call
reCAPTCHA v3Nothing; you get a scoreMost automated postingYes, verify and pick a threshold
hCaptchaSometimes a puzzleMost automated postingYes, 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.

Live demo: honeypot plus time trap

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.

Copy the honeypot and time trap

The HTML goes in your form. The server check is shown for Node with Express, but the logic is three lines in any language.

Form with a honeypot field

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>

Server check (Node, Express)

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");
});

Adding Turnstile, reCAPTCHA or hCaptcha

All three work the same way. These details come from each provider's own documentation.

CAPTCHA services side by side
TurnstilereCAPTCHA v3hCaptcha
Scriptchallenges.cloudflare.com/turnstile/v0/api.jsLoaded from Google with your site keyjs.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 atchallenges.cloudflare.com/turnstile/v0/siteverifyGoogle's verify endpointapi.hcaptcha.com/siteverify
Token life300 seconds, single useTwo minutesSee provider docs
Resultsuccess true or falsesuccess plus a 0.0 to 1.0 scoresuccess true or false

Verify a Turnstile token (Node 18 or later)

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;
}

Why the free checks come first

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.

Hide the trap the right way

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.

Verify CAPTCHA tokens on the server

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.

How it works

  1. Add one extra input named something tempting, like website, and hide it with CSS rather than type=hidden.
  2. Add a hidden field holding the time the form was shown.
  3. On the server, drop any submission where the trap has a value or the time gap is under a few seconds.
  4. If spam continues, add a CAPTCHA widget and verify its token on the server before saving or emailing the message.

Tips

Frequently asked questions

What is a honeypot field?

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.

Is there a free reCAPTCHA alternative?

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.

Does reCAPTCHA v3 show a puzzle?

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.

Can the widget alone stop spam?

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.

Why do I still get spam with a honeypot?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap