DEV Community

Harsimran jeet Singh
Harsimran jeet Singh

Posted on Originally published at formkove.com

How to stop contact form spam

Contact form spam comes from three places:

  1. Bots that fill in every field they find on the page.
  2. Scripts that POST straight to your form's URL, never loading the page at all.
  3. People and AI tools writing real-looking messages: SEO offers, "I noticed your website…" pitches, crypto and loan offers.

Each defense below stops some of these and misses others. The cheap ones go first.

1. Honeypot field

Add a field that people never see. Bots fill it in; people leave it empty. If it has a value, drop the submission.

<div class="hp" aria-hidden="true">
  <label>Leave this empty <input type="text" name="website_url" tabindex="-1" autocomplete="off"></label>
</div>

<style>
  .hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
</style>
Enter fullscreen mode Exit fullscreen mode

Tips:

  • Use a believable name like website_url or company_fax. A field called honeypot is easy for a bot to skip.
  • Hide it with a class, not only display:none inline: some bots skip inputs with inline hiding.
  • tabindex="-1" and aria-hidden keep keyboard and screen reader users out of it. autocomplete="off" stops password managers and autofill from filling it.
  • Reject it quietly: answer as if it worked, so the bot doesn't learn to avoid the field.

Stops: simple bots. Misses: scripts that POST directly (they only send the fields they know), and anything human-written.

2. Time check

People take more than a few seconds to fill in a form. Many bots submit instantly. Record when the form loaded and reject anything sent too fast.

<input type="hidden" name="loaded_at" id="loaded_at">
<script>
  document.getElementById("loaded_at").value = Date.now();
</script>
Enter fullscreen mode Exit fullscreen mode

On your server, reject the submission if Date.now() - loaded_at is under about 3 seconds, or if the field is missing. This needs your own backend, and a bot that waits or fakes the value gets through, so treat it as a filter for lazy bots only.

3. Rate limits

Limit how many submissions one IP address can send per minute and per hour. It won't stop a single spam message, but it stops one bot from sending hundreds and keeps a flood from filling your inbox or your plan's quota. Most form backends and hosting platforms do this for you; on your own server, put the limit in front of anything that sends email.

4. A challenge: Turnstile, reCAPTCHA or hCaptcha

A challenge widget checks the browser and gives the form a token. Your server then verifies the token with the provider. This stops scripts that POST directly, since they have no valid token.

Cloudflare Turnstile Google reCAPTCHA hCaptcha
What visitors see Usually nothing, sometimes a checkbox v2: checkbox and image puzzles. v3: nothing, you get a score Checkbox, sometimes image puzzles
Privacy No ad tracking, no cookies for tracking Runs on Google and uses Google cookies Privacy-focused
Cost Free Free up to a monthly allowance, then paid through Google Cloud Free tier, paid plans for more

Turnstile is the easiest choice for most contact forms: free, rarely visible, and it doesn't send your visitors to Google.

Add the widget:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form action="/contact" method="POST">
  <!-- your fields -->
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
  <button type="submit">Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Then verify the token on your server. The widget alone does nothing: a bot can skip it and post the form anyway.

const result = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
  method: "POST",
  body: new URLSearchParams({
    secret: process.env.TURNSTILE_SECRET_KEY,
    response: formData.get("cf-turnstile-response"),
  }),
}).then((res) => res.json());

if (!result.success) {
  // reject the submission
}
Enter fullscreen mode Exit fullscreen mode

Stops: bots and direct POSTs. Misses: people pasting a pitch into your form by hand, and AI agents driving a real browser.

5. Content filtering

What gets past everything above is usually written by a person or by an AI tool: a real browser, a real pause, a valid token, and a message nobody wants. To catch it you have to look at what the message says.

  • Rules: block messages with more than a few links, or with a link in the name field. Cheap, and spammers adapt.
  • Akismet: a spam-checking service from Automattic, built into many WordPress setups. Free for personal sites, paid for commercial ones.
  • AI classification: a model reads each submission and scores how likely it is to be spam. It catches pitches that rules miss, because it judges the message the way you would.

Any content filter makes mistakes sometimes. Quarantine, don't delete: hold flagged messages somewhere you can check, and let a real lead through with one click.

6. Validate on the server

Whatever you add in the browser, check it again on the server: required fields present, a valid email address, reasonable lengths, and only the fields you expect. Bots don't run your JavaScript, so browser-side checks don't count.

What to use

Situation Use
A personal site with a little spam Honeypot + rate limits
A business site, or spam reaching you daily Honeypot + Turnstile + rate limits
Spam that looks hand-written (SEO and sales pitches) Add content filtering, with a quarantine you check weekly
Forms where a lost lead costs money All of the above, and never auto-delete

Order matters for cost too: let the honeypot and Turnstile reject bots first, so only messages from real browsers reach the content filter.

Doing this with Formkove

If you'd rather not build it, a form backend does this for you. With Formkove:

  • Rate limits are on for every form: 10 submissions per minute and 30 per hour per IP.
  • The AI spam filter checks every submission before anything is sent, on every plan including Free. Spam is held in your dashboard, and Not spam, deliver sends a false positive on to its destinations.
  • Honeypot: set a field name on the form's AI & Spam page and add the hidden input.
  • Turnstile: paste your own site and secret keys; Formkove verifies the token on every submission.

Submissions the AI flags still count toward your monthly limit. Submissions stopped by the honeypot or Turnstile don't, so turn both on if a form attracts a lot of bots. Setup steps are in the spam protection docs, and the free plan includes 300 submissions a month.


Originally published on formkove.com.

Top comments (1)

Collapse
 
steven_mock_afbb801174fc3 profile image
Steven Mock •

Really useful framing — that each defense stops some spam and misses the rest, so the order matters. For small-business and property sites, the point about quarantining instead of deleting is the one I'd underline. A lost lead costs far more than a few junk messages to skim once a week, and the hand-written SEO pitches are exactly the ones that slip past a honeypot. I also appreciated the reminder that a Turnstile widget alone does nothing without server-side verification. Honeypot plus rate limits first, then a challenge, then filtering — that's a sensible, low-friction stack.