← All posts Measurement

111 Submissions, One Delivered, None Real

Four months of contact form data from a small consulting site: what the spam actually looks like, how a filter that logs its own rejections lets you audit it, and the uncomfortable thing the numbers say about the form itself.

This site's contact form has received 111 submissions since May 2026. Exactly one was delivered to my inbox. That one was also spam.

Not a single genuine inquiry has ever arrived through it.

That sounds like a story about spam filtering, and partly it is. But the more useful part turned out to be a design decision made early and almost by accident, and a conclusion about the form itself that I did not expect and did not enjoy.

What 111 submissions actually look like

Every submission is stored, including the ones that never reach my inbox, along with the specific rule that rejected it. Four months, categorized:

Why it was rejectedCount
Honeypot field filled in36
SEO metrics pitch (DR/DA)25
Unsubscribe boilerplate in the body16
Timing trap — submitted 1 second after render14
Link exchange pitch4
Promotional pitch (link plus keywords)4
Timing trap — submitted 0 seconds after render4
Link shortener2
Link shortener, short-domain shape2
HTML anchor tags in the message1
Timing trap — no render timestamp at all1
Unpronounceable string1
Delivered1

A few of those deserve a note.

The honeypot is the cheapest thing on the list and catches the most. It is a text input that is positioned off-screen and marked aria-hidden, so no human and no screen reader ever encounters it. Anything that fills it in is a program walking the DOM. Thirty-six submissions, no third-party service, no CAPTCHA, nothing for a real visitor to solve.

The timing trap caught 19. The form carries a hidden timestamp of when the page rendered; the handler compares it against arrival. Four submissions arrived in the same second the page rendered. One carried no timestamp at all — that one announced itself in the user agent as curl/8.7.1, which is a refreshingly honest way to be a bot.

The content rules are the interesting half, because they have to catch a slower adversary: a human, or a good enough imitation of one, typing into the form on purpose. Those are the SEO pitches, the link exchanges, the "I noticed your website could rank higher" messages. They clear the honeypot and the timer because they are not in a hurry.

The one that got through

On September 10, 2026 a submission was delivered offering to restore websites from the Wayback Machine. It was spam, but it was well-behaved spam: no honeypot, plausible timing, no shortener, no unsubscribe footer, no SEO jargon.

Two days later a near-identical message arrived from a different sender and was blocked, because a rule had been added in between. That is the system working as intended, which is a nicer way of saying the first one was the cost of learning.

The part that actually matters: storing the rejections

Most spam filtering silently discards what it blocks. That is the natural implementation — the whole point is that you never see it — and it creates a permanent blind spot.

If you discard silently, then "I have received no inquiries" and "my form has been eating inquiries for four months" produce identical evidence. You cannot tell the difference, ever, and the second possibility is quietly expensive.

So every submission is written to the database whether or not it is delivered, with the exact rule that rejected it. That single decision converts an unanswerable question into a ten-minute review. I read them roughly weekly.

What that audit caught

On September 13, 2026 a message arrived reading:

Hello, I'm curious what your hours of operation are? Also, do you know when you...

It was blocked by the timing trap, and it is by far the most human thing the filter has ever caught. A plain question, no links, no pitch, no jargon. If that was a real person, the form had just cost me a genuine inquiry.

Three things said otherwise. It was submitted zero seconds after the page rendered, which a human cannot do. The display name did not match the email domain. And "what are your hours" is a known reply-bait template — the goal is a human response, which confirms the address is live and monitored.

The case closed itself the following day, when a second message arrived from the same domain, under a different name, asking the same question in slightly different words. Not a person. A campaign.

Without stored rejections I would never have seen either one, and the doubt would simply have sat there.

The uncomfortable conclusion

Here is the part I did not expect. Over 30 days, session recording showed nine visitors opened the contact form. Analytics recorded one of them starting to type. Zero submitted.

Meanwhile 63 spam submissions arrived in the same window.

So the honest reading of four months of data is not "the spam filter works", satisfying as that is. It is that automated systems complete the form reliably and humans do not. Every rule above is solving the easier problem.

Some of that turned out to be mechanical. The form was a modal that auto-focused its first field on open, which on a phone throws up the keyboard instantly and covers half the screen — one recorded visitor opened it and closed it nine seconds later without typing. The modal also had no maximum height, so on a short screen the submit button could sit below the fold with nothing to scroll. And the form had no URL of its own, so there was nothing to link anyone to.

Those are fixed now. Whether that changes the number is an open question, and I will know in a few weeks.

If you take one thing

The filter rules are worth copying — the honeypot in particular is close to free. But the decision that paid for itself was storing every rejection with its reason.

A filter you cannot audit is a filter you have to trust. Four months in, I do not have to trust mine, and the one message that genuinely looked human turned out to be provably not. That is worth considerably more than the rules themselves.