A form spam filter on AWS for a few dollars a month
A contact form with no protection fills with rubbish, and the usual response is a CAPTCHA. That trades a visible cost on every genuine visitor against an invisible one on nobody, and the measurable drop in completions is frequently worse than the spam. This post walks through a small system that gets most of the benefit without asking your customers to identify traffic lights.

Key takeaways
- A missed lead costs far more than a spam message. Every threshold follows from that.
- Cheap structural signals classify most submissions for free, before any model runs.
- Three destinations: inbox, review queue, quarantine. Nothing is deleted.
- No CAPTCHA. The visible cost falls on genuine visitors and the spam adapts anyway.
- Designed on AWS for about $2 a month.
The whole system on one page
Before any code, here is the shape of what we are designing.
- App integration
- Machine learning
- Security & identity
- Analytics
- Front-end & mobile
- People
The asymmetry
A small business’s contact form produces perhaps forty genuine enquiries a month and four hundred spam messages. The genuine ones are worth, on average, a meaningful fraction of a job. The spam costs about four seconds each to delete.
So the arithmetic is not close: deleting one real enquiry to save four hundred deletions is a bad trade by an enormous margin. Every threshold in this system is set from that, which is why the aggressive-looking decision — quarantine — still keeps everything and is searchable.
What runs on every submission (the inside)
- The cheap scorer. Half a dozen structural signals that cost nothing: a honeypot field, how long the form was open, whether the message contains links, whether the fields were filled in a plausible order. Part 2 covers each and how much each is worth.
- The model band. For the submissions the cheap signals cannot place, one call asking a narrow question: is this a person describing a need, or a template? Part 3 is about keeping that band small.
- The router. Three destinations. Confident enquiry goes to the inbox immediately. Ambiguous goes to a review queue that somebody glances at twice a day. Confident spam goes to quarantine, retained and searchable.
One submission, end to end
- App integration
- Machine learning
- Analytics
- Front-end & mobile
- People
In plain words
Somebody fills in the contact form asking about a bathroom. They took ninety seconds, left the hidden field empty, filled the fields in a sensible order, and wrote three sentences with no links in them. Every cheap signal says person, so it goes straight to the inbox with no model call and no delay.
A minute later a bot submits. It filled the hidden field, completed the form in under a second, and the message is two hundred words with four links about search engine optimisation services. Every cheap signal says bot, so it goes to quarantine, again without a model call.
The interesting one arrives on Tuesday: two lines, no links, a plausible name, a free-mail address, filled in eleven seconds. It might be somebody in a hurry on a phone or it might be a better bot. The cheap signals genuinely cannot say, so one model call asks whether it reads like somebody describing a specific need. It says probably, with middling confidence, so it goes to the review queue — and somebody glancing at that queue after lunch spends two seconds deciding it is a real job.
Design rules that shaped every decision
- Never delete. Quarantine is retained and searchable, because “I filled in your form and heard nothing” is a conversation that happens.
- No CAPTCHA. The cost falls entirely on genuine visitors and bots solve them anyway.
- Cheap signals first, always. They are free, deterministic, and decide most submissions.
- Keep the model band narrow. It is the expensive part and the least predictable part.
- When uncertain, hold rather than drop. A two-hour delay is a much cheaper error than a lost lead.
- Every decision is recorded with the signals behind it, so the thresholds can be tuned from evidence.
Why this shape
Spam filtering has a well-known failure mode: it works, everybody stops thinking about it, and six months later somebody discovers a folder of real enquiries. The reason is always the same — a filter confident enough to delete, and nobody watching the false positive rate because false positives are invisible by construction.
So this design refuses the deletion. Everything is kept, the ambiguous band goes to a human queue rather than a coin flip, and the quarantine is searchable by name and email so that the one conversation that reveals a false positive can actually be resolved. It is a slightly worse filter and a considerably better business decision.
The next four posts walk through each piece: the cheap signals, the model band, how a borderline reaches a person, and how the thresholds get tuned. One diagram per post, a cost breakdown, and an engineering reference at the end.
All posts