send.webforms.toRead the docs →

guidesspam

How to stop contact form spam: nine layers, cheapest for your visitors first

Honeypot, time trap, rate limits, domain lock, content signals, reputation, an AI classifier and, last of all, a captcha. What each one catches, what still gets through, and why we hold suspected spam instead of deleting it.

WebForms 10 min read

Most advice about contact form spam starts with a captcha. We think that is backwards. A captcha is the only defence here that every real visitor pays for, in time, in frustration and usually in data sent to a third party, so it belongs at the end. This guide goes through the layers ordered by what they cost the visitor (not by when they run): what each catches, what slips past it, and how WebForms handles it.

Where contact form spam actually comes from

It helps to know what you are filtering, because each layer below catches a different kind of sender.

  • Form-filling scripts. Programs that fetch a page, find every input in a form and post plausible text into all of them. They never render the page, so they cannot tell a visible field from a hidden one. The cheap, high-volume end.
  • Scripted browsers. Headless browsers that load the page, run its JavaScript and fill in only what a person would see. Invisible to anything that relies on the page never being rendered.
  • People. Someone paid to paste the same pitch into thousands of contact forms: SEO services, app development, "I noticed a few issues with your website". A real browser, a real typing speed, often a real email address. Nothing that tests for a robot stops them, because they are not one.

Each layer is good against one or two of these and useless against the rest, which is why spam protection is a stack and not a single setting.

1. A honeypot field (costs visitors nothing)

A honeypot is an input that people never see and scripts fill in anyway. If it arrives with a value, a script sent it. It is the oldest trick here and still the best value: no friction, no third party, nothing leaves the page.

honeypot
<div aria-hidden="true" style="position:absolute; left:-9999px;">
  <label for="hp-field">Leave this field empty</label>
  <input type="text" id="hp-field" name="_gotcha" tabindex="-1" autocomplete="off">
</div>

Three details decide whether it works. Hide it with CSS, not type="hidden", which well-written scripts skip for exactly this reason. Keep it out of the keyboard order and away from screen readers (tabindex="-1" on the input, aria-hidden on the wrapper), or you have built a trap for the visitors you least want to trap. And give it a name autofill will not recognise: a honeypot called email or phone can be filled in by a password manager, and a real person's message is thrown away.

In WebForms the honeypot field is always _gotcha, a name autofill has no reason to touch. A filled honeypot is rejected outright: not stored, not delivered, and the sender sees the normal success response, because telling a bot what caught it is how it learns. What it misses: anything that renders the page, and any script written to skip a field name as widely used as _gotcha. That is why it is the first layer and not the only one.

2. A time trap (costs visitors nothing)

A person needs several seconds to read a form and type into it. A script needs milliseconds. Record when the form rendered, compare it with when the submission arrived, and anything impossibly fast was not typed by hand.

time trap
<input type="hidden" name="_start_time" id="start-time">
<script>
  document.getElementById("start-time").value = Date.now();
</script>

WebForms rejects a submission that arrives faster than the form's minimum fill time, about two seconds by default and configurable per form. That catches scripted browsers that fill and submit in one motion. The limit is built in: the timestamp comes from the page, so a script that posts straight to the endpoint leaves nothing to measure, and a patient one simply waits. Keep the minimum low, because autofill can complete a short form faster than you would guess, and a time trap that catches people is worse than none.

3. Rate limiting (costs visitors nothing, almost always)

Rate limiting ignores content. It counts how fast submissions arrive from one source and refuses the excess, which stops a single script posting ten thousand times overnight however convincing each message looks. The "almost" is shared networks: an office or a university can put many people behind one address, so the limit has to be generous.

WebForms limits how fast one source can submit to a form and answers the excess with HTTP 429 and a Retry-After header; refused submissions are not stored. A related decision matters as much: quarantined spam does not count toward your monthly quota, so a bot that gets past the rate limit still cannot spend your allowance.

4. Domain lock (costs visitors nothing)

A form backend's endpoint key sits in your page source by design, so anyone can copy it. An allowed-origins list says which sites may use it: browsers attach an Origin header to a cross-site form post, and a submission from an origin not on your list counts against it. In WebForms this is the form's allowedOrigins setting, with wildcards such as *.example.com, and a mismatch adds to the spam score.

Be clear about what it is for. The Origin header is a browser courtesy: it stops your form being reused on someone else's website, but a script talking to the endpoint directly can send any header it likes. Domain lock is a fence against other sites, not a bot detector.

5. Disposable email addresses (a small cost to a few visitors)

Throwaway inbox services hand out addresses that last minutes. Bots use them, and so do careful people avoiding another marketing list, which is why WebForms adds a disposable address to the score rather than rejecting it. Detection is a list problem, and the hard cases are services that issue a fresh random subdomain for every address, so the check matches the parent domain as well as exact names.

6. Content signals: links, markup and your own blocklist (a small risk of false positives)

Now the filter starts reading. A message with a fistful of links in two lines, raw HTML anchor tags typed into a plain-text box, or text that switches alphabets in the middle of a word (a trick for slipping past keyword filters) is almost never a genuine enquiry. On top of the built-in checks, each form can carry its own blocked keywords and blocked sender domains, which is the right home for the one recurring nuisance that is specific to your business.

The cost is false positives. A designer sends three links to their portfolio; a customer pastes a snippet of HTML to show you a bug. That is why these checks add to a score instead of deciding alone. WebForms quarantines at a score of 0.6, a level at which one weak signal passes and two agreeing ones are held. You do not set that number yourself; what you control per form is which checks run and what goes on the blocklists, because a job application form and a support form put very different prices on a wrong call.

7. Sender reputation (costs visitors nothing)

A filter that forgets everything between submissions pays to relearn the same sender every time. WebForms remembers senders by the identity they control, their email address and its domain, and never by IP address, because IP addresses are shared and reassigned, and punishing one punishes strangers. Only the AI classifier's own verdicts build that reputation: a sender it has confirmed as spam scores worse on the next attempt, before the classifier is asked again. It catches repeat senders, and does nothing for a first contact or for someone who rotates addresses.

8. An AI classifier (costs visitors nothing, costs some privacy)

Everything so far looks at shape: links, timing, addresses. A polite paragraph offering SEO services, with no link and no phrase on any list, has no shape; it looks like an enquiry until you read it. Reading it is the job of a classifier, the only layer here that catches the human spammer.

WebForms runs one on every plan, including free, on every submission the rule-based score has not already condemned, including those that score zero. We measured it on 22 September 2026, before switching it on for everyone, over the 808 submissions stored at the time: a median response of 258ms (326ms at the 95th percentile), no failed calls, agreement with 50 of the 51 submissions the rules had already quarantined, and 11 delivered submissions flagged as spam with at least 90% confidence. None of the 808 had a human spam label, so these are agreement figures, not precision and recall.

It has two costs. The message is read by a third-party service, listed on our privacy page, so if no third party should ever see a form's content you can switch the classifier off for that form and the rule-based filter runs alone. And it will sometimes be wrong, which is what quarantine is for.

9. A captcha, last (costs every visitor something)

A captcha asks the visitor to prove they are a person, either by solving something or by letting a script inspect their browser. It is good at stopping the scripted browsers the earlier layers missed. It is useless against the human spammer, who solves it like anyone else. And it is the only layer on this list that every genuine visitor has to pass through: a few seconds, an occasional puzzle, a real barrier for some disabled visitors, and with most providers a request to a third party before your form can be sent.

So add one to a specific form that is still being hit after everything above, not to every form by default; our comparison of honeypots, reCAPTCHA, hCaptcha and Turnstile, linked below, covers which to pick. WebForms is designed so that a form does not need one: every layer above runs on the server, on every plan, with nothing for the visitor to solve.

What still gets through

With every layer on, what still reaches the inbox is mostly what a person wrote: an agency pitch in fluent English, a link-free message from a real address about something you do not want. The classifier is the layer aimed at it (in the September evaluation it flagged 11 submissions the rules had let through), but no filter knows your business. A cold email from a genuine local supplier is spam to one owner and a lead to another, and that call is yours. For the pitch that keeps coming back in the same words, a blocked keyword on that form is the right tool.

Why we quarantine instead of deleting

Every layer above trades missed spam against wrongly blocked people, and no setting makes both zero. So the question we designed around was not how to never be wrong, but what being wrong costs. If a wrong verdict deletes a message, a false positive is a lost customer you never hear about. If it holds the message, the cost is a minute of review.

So WebForms splits verdicts in two. A filled honeypot, a submission faster than the time trap and a narrow set of content with no legitimate reading on a contact form are rejected outright and never stored, because none of them is a judgement call. Everything else that scores at or over the threshold is quarantined: stored, visible in a separate spam view in the dashboard, never delivered to any destination, and not counted toward your monthly quota.

Quarantine has limits worth knowing before you rely on it. There is no release button that sends a held submission on to your destinations: if the filter holds back a real enquiry, you find it in the dashboard and reply yourself. Nobody is notified when something is quarantined, so it only protects you if someone looks; once a week is plenty for most forms. Quarantined submissions follow your plan's retention period like everything else, and the Submissions API leaves them out unless you ask for status=quarantined.

A setup that works for most contact forms

  1. Add a honeypot with a name autofill will not touch, hidden with CSS and kept away from keyboards and screen readers.
  2. Stamp the render time into the form so the time trap has something to measure.
  3. Lock the form to your own domains.
  4. Leave the content checks and the AI classifier on, and add the phrases that keep coming back to the form's blocked keywords.
  5. Check the spam view weekly for the first month. If real messages are landing there, loosen that form: a blocked keyword that is too broad, or disposable-email checking on a form where privacy-minded people write in. If the same junk keeps getting through, add its phrases to the blocked keywords.
  6. Add a captcha only to a form that is still being hit after all of that, and prefer one most visitors never see.

Before any of it, paste your form into the free spam test linked below. It checks the markup for a honeypot, an HTTPS action, a POST method and any email address sitting in the page for scrapers to collect, and nothing you paste leaves your browser.

Get your endpoint

Free for 300 submissions a month. No card required.

Start free