5 min

Protecting a form from spam without a Captcha: from Akismet to my homegrown anti-spam

2026-05-06

Back in November 2016, I published a post on my WordPress blog titled “How do you set up Akismet?”. I explained how to enable the anti-spam plugin bundled with WordPress, grab an API key from the official site, then choose between deleting unwanted comments outright and a fourteen-day probation folder. I even shared a small filter to drop into functions.php to extend that window to thirty days, with a line that already said it all: “we are never safe from a false positive.” Ten years on, I no longer protect blog comments but the forms on my own site, built on Pulsar, my PHP framework. And that 2016 instinct, keeping a doubtful message rather than losing a good one, has become the central rule of the anti-spam I ended up writing myself.

What Akismet taught me

Back then, I delegated everything. Akismet combined a dictionary of unwanted keywords, a set of rules and a blacklist, and sorted comments into pending or spam. For a blog, it worked well and I recommended it without reservation.

But delegating has a price. Every message travels to third-party servers for analysis, which, on a contact form where a prospect leaves their name and email, has become a real GDPR concern. And above all, the cost of a false positive has changed scale: a lost blog comment is a shame. A lost quote request is a client who will not come back. My old site eventually buckled under contact-form spam, and while rethinking the new one I set a starting constraint: filter seriously, without ever sending humans the bill.

Why no visible Captcha

The usual answer to spam is a Captcha. I ruled it out for three reasons.

Friction, first. Every step added before submission makes real visitors give up, while serious bots solve these puzzles or farm them out to click farms. You lose humans to slow down machines that are no longer slowed at all.

Accessibility, next. Spotting traffic lights in blurry thumbnails excludes people with low vision, and the audio alternatives are painful for everyone.

Privacy, last. The big-platform Captchas load third-party scripts, watch how the visitor behaves and raise consent questions I did not want to deal with. My site calls no external service for its forms: everything is self-hosted, nothing leaves.

A Captcha punishes the human for a problem created by bots. I wanted the opposite: checks that only bots notice.

Five invisible layers instead of a wall

No single check is perfect on its own. Stacked together, they stop the bulk of automated spam without the visitor ever seeing a thing. I am deliberately staying at the level of the concept: publishing field names or exact thresholds would amount to handing out the bypass manual.

The honeypot. A field invisible to humans that bots fill in because they fill in everything. The detail that matters: the site tells the bot “message sent” without sending anything. It does not know it was caught, so it learns nothing.

The timing trap. When the form is rendered, the server issues a cryptographically signed timestamp. A human takes seconds, often minutes, to read and fill in a form. A bot posts almost instantly. The signature makes the token impossible to forge, and the layer only blocks on positive proof of automation: a missing or damaged token never blocks, the other layers cover that case. It works without JavaScript.

The invisible challenge. A small cryptographic computation (proof of work) that the browser solves on its own in the background, in a fraction of a second, while the visitor types. Imperceptible to a human, expensive for a bot posting in bulk. Self-hosted, with no API key or third-party service. And if the browser does not run JavaScript, the missing answer does not block submission: the server-side layers take over. Graceful degradation applies to security too.

Rate limiting. A bot flooding the form from the same address is throttled after a few attempts. The IP address is hashed before any storage, I never keep it in the clear.

Content scoring. Link density, text quality, duplicate detection. Each signal adds points to a score, and this is where the philosophy parts ways with classic filters: that score never rejects.

The zero lost lead rule

My pipeline separates two families of checks. Positive proof of automation, like the honeypot or the timing trap, rejects. Content signals never reject: a high-scoring message is still delivered, simply flagged for manual review in my inbox.

A real client who pastes three links to their existing site, or writes two curt lines from their phone, gets through. I would rather read two spams than lose a request. That is exactly the logic of my thirty-day Akismet probation in 2016, pushed all the way: doubt always favors the message.

What it looks like in practice

On the contact page and on the quote calculator, the visitor sees none of this. No “I am not a robot” checkbox, no puzzle, no consent banner for a third-party script. The form fills in and sends, that is it.

If your own form is buckling under spam, my suggestion fits in one sentence: before adding a Captcha that will drive your visitors away, stack invisible checks and reserve rejection for proof of automation. In 2016, I ended my post by inviting you to comment and share. In 2026, the honest version is simpler: if the topic speaks to you, write to me through that very form. It will hold up.