A few days after a contact form goes live, the bot traffic starts. The first instinct is to slap a CAPTCHA on it — but a CAPTCHA has a cost: some share of your real customers will bail on that puzzle. If the form doesn't get many submissions to begin with, blocking spam ends up blocking customers too.
The layered approach below cuts most of the spam without adding any friction for the user. It's what I use on this site's own contact form.
Layer 1 — the honeypot
Simple and surprisingly effective. Add a field to the form that humans can't see. Most bots parse the form and fill in every field. A field that comes back filled in tells you the sender is a bot.
Critical detail: don't hide the field with type="hidden" — bots recognize that. Push it off-screen with CSS and skip it for screen readers instead:
<div style="position:absolute; left:-9999px;" aria-hidden="true">
<label>Your website</label>
<input type="text" name="website" tabindex="-1" autocomplete="off">
</div>
One line of server-side logic:
if (!empty($_POST['website'])) {
// Pretend it succeeded — don't tip off the bot
header("Location: /contact.php?status=success");
exit;
}
Note: don't tell the bot "caught you." If you behave as if it succeeded, the spam tool assumes the form worked and won't bother changing its strategy.
Layer 2 — a time threshold
A human can't fill out a contact form in 2 seconds. A bot can. Carry the timestamp of when the form was rendered in a hidden field and check the difference on submit:
$elapsed = time() - (int)($_POST['form_ts'] ?? 0);
if ($elapsed < 3 || $elapsed > 3600) {
// Too fast: a bot. Too slow: a stale session.
exit;
}
Sign the timestamp (with hash_hmac) — otherwise a bot just generates the
value itself.
Layer 3 — IP-based rate limiting
Ten form submissions an hour from the same IP isn't normal human behavior. Log the IP and timestamp on every submission, and reject anyone who exceeds the window you set.
A JSON file is enough for small sites; move to a database or Redis once traffic grows. Two warnings:
- Concurrent writes: use a lock (
LOCK_EX) when writing to the file, or two requests can overwrite each other's record. - Shared IPs: users behind the same office network or mobile carrier can share an IP. Don't set the limit too aggressively — 3-5 submissions an hour is a reasonable start.
- Keep the log file outside the web root. If you're keeping it inside the document root, block access via
.htaccess— it contains visitor IPs, and that's personal data.
That log file grows over time and quietly bloats if nobody's watching it. Add regular cleanup to your maintenance routine as a line item.
Layer 4 — server-side validation
The required and type="email" attributes in your markup are
for user experience, not security. A bot doesn't use a browser — it hits the server
directly. Re-validate every field server-side:
- Is the email actually valid format? (
filter_var($mail, FILTER_VALIDATE_EMAIL)) - Do fields have a length limit? A thousand-character "name" field is suspicious.
- Is the number of links in the message reasonable? Three or more links is a classic spam signature.
- Do fields contain a line-break character (
\r\n)? That's an email header injection attempt — always strip it.
What you tell the user when validation fails also matters. Rather than "something went wrong," show the problem and the fix — a principle I go into further in the admin panel post.
Layer 5 — CAPTCHA, but the invisible kind, if you still need it
The four layers above are enough for most sites. If you're still getting targeted despite that, add a CAPTCHA — but choose the invisible version that runs in the background rather than making the user solve a puzzle. It adds a layer without breaking the experience.
A suggested order
You don't need to build all of this at once. Add layers in sequence and measure spam at each step: the honeypot alone usually causes a big drop, the time threshold catches most of what's left. You may not even need the third layer.
Adding layers without measuring means unnecessary complexity and the risk of blocking a real customer. The form's actual job isn't to stop spam — it's to get the customer to you.