
When bot submissions started appearing through the contact form on Cordinant, the obvious option was to add CAPTCHA.
I wasn't convinced that should be the first response.
For a small product website, I wanted the normal experience to stay simple: open the contact page, write a message, send it. I didn't want every legitimate visitor to go through another third-party check unless the traffic actually justified it.
So I approached the problem from the server side instead.
The form now has several small checks rather than one big anti-bot mechanism.
There is a hidden honeypot field that a human should never fill. The server records when the form was opened, so an almost instant submission becomes suspicious. A CSRF token makes a completely blind POST request slightly harder. The submitted values are validated again in PHP rather than trusting browser validation.
Then there is rate limiting.
That became important when I thought about a bot that was slightly smarter. It could GET the real form, preserve the session, leave the honeypot empty, wait five seconds and then POST valid-looking data.
At that point, the early checks may all pass. But repeatedly sending messages from the same source creates another signal.
The part I found interesting is that none of these checks is particularly sophisticated by itself. The useful part is the combination.
SMTP also comes last. A request has to pass the cheap checks before the application is allowed to open an authenticated SMTP connection and send anything to my inbox.
I still wouldn't say this is a universal replacement for CAPTCHA. If the traffic changes, something like Cloudflare Turnstile could become the next layer.
For now, I prefer starting with protection that normal visitors don't notice.
Optional further reading: https://cordinant.com/blog/reduce-contact-form-bot-spam-without-captcha
What has worked best for you on smaller sites: invisible server-side checks first, or adding something like Turnstile from the beginning?