ElmsPark Guides
Security guide

Stopping contact-form spam without CAPTCHA

CAPTCHA puts the burden on your visitors. The better approach is five server-side layers, each invisible to a real person, that catch bots before the message ever reaches your inbox. No puzzle, no friction, no third-party widget.

🛡 No CAPTCHA required 👁 Invisible to real visitors ⚙ Any site, any backend 🧩 Built into EP Email
What you’ll need: access to your form handler on the server side. These layers all run when the form is submitted, not in the browser. No external accounts or third-party APIs are required for the first four layers. The fifth layer (the shared blocklist) is optional and needs a token.
Why not just add a CAPTCHA? CAPTCHAs add friction for every real visitor in order to stop bots. They hurt accessibility, slow conversions, and leak data to the CAPTCHA provider. Modern bots and commercial solving services bypass them in seconds anyway. The layered server-side approach described here is invisible to humans and stops the vast majority of automated spam without a single puzzle.
▶ Prefer to learn it interactively? Tap through the interactive lesson, one idea at a time, about two minutes, with quick questions as you go.

Use this guide with any AI assistant

Download it as a prompt file, paste it into Claude, ChatGPT, Gemini or any LLM, and it will walk you through implementing each layer one step at a time.

↓  Download as LLM prompt

1The honeypot field

A honeypot is a form field that is hidden from human visitors but visible to bots. Real people never fill it in because they never see it. Bots fill in every field they find, and that is their undoing.

The implementation is simple:

Real people cannot see the field, so they never fill it in. A bot filling in every visible and invisible field triggers an immediate rejection, at zero cost.

Why hide it with CSS, not JavaScript? CSS hiding needs no script to run: every human browser renders the CSS and never shows the field, while the input stays in the raw HTML for a bot to find and fill. JavaScript hiding is less reliable: it depends on the script executing, it can briefly flash the field on screen, and a visitor with JavaScript disabled would see the field and might fill it in by mistake. Keep the honeypot purely CSS-hidden.

2The time trap

A human filling in a contact form takes at least a few seconds: they read the labels, type a message, and click submit. Automated bots often submit in under a second.

The time trap works like this:

This layer catches a different class of bot from the honeypot: scripted submitters that understand hidden fields and leave them empty, but still fill and submit faster than any human could.

Tuning the threshold. For a short form (name and email only), a 2-second minimum may be enough. For a longer form with a message field, 4 or 5 seconds is fairer. If you start seeing false rejections from real visitors on slow connections, lower the threshold slightly. The goal is to fail closed on bots and open on humans, so err towards a lower number and raise it if spam still gets through.

3Rate limiting by IP

The honeypot and time trap stop most single-shot bots. Rate limiting stops the ones that retry or that run in slow-send mode to evade timing checks.

The principle is straightforward: track how many submissions have arrived from each IP address in a rolling time window. Reject any submission from an IP that has already submitted recently.

What about shared IPs? Corporate offices, schools, and some mobile networks route many people through a single IP. A 5-minute window is short enough that legitimate repeat submissions from the same IP (two colleagues hitting the same form) will be rare. If you serve a high-volume audience from a known shared network, consider raising the limit to allow, say, 2 submissions per IP per window rather than 1.

4Content heuristics: link caps and keyword blocklists

Some spam bypasses the mechanical filters above. The message arrives at human speed, from a fresh IP, with the honeypot untouched. The giveaway is in the content.

Capping the number of links

SEO spam and link-building pitches almost always include several URLs. Legitimate enquiries rarely include more than one or two. Setting a maximum number of http:// or https:// links across all fields is a low-false-positive way to catch this category.

A limit of 2 links is enough to stop most link-building spam while allowing a real visitor to share a couple of reference URLs in their message. Set it to -1 to disable the check entirely if your use case routinely involves more links.

Keyword blocklists

A blocklist is a list of phrases that, if found anywhere in the submitted text, cause an automatic rejection. The match is case-insensitive and matches substrings, so casino also matches online casino and Casino Royale.

Common starters for a contact-form blocklist:

bitcoin
crypto
casino
seo services
guest post
backlink
link building
rank higher
viagra
cialis

The list works best when it is tuned to the spam you are actually receiving. Keep it short and specific: an overly long list risks false positives and becomes hard to maintain. Review it every few months and drop anything that is no longer appearing in your spam.

Run the content checks after the cheap ones. Keyword matching and link counting scan through text fields, which is slightly more work than checking a hidden field or a timestamp. Run them after the honeypot and time trap, so the majority of spam is already rejected before any text scanning begins.

5A shared blocklist, the network effect

The layers above are all local: they learn from your own submissions. A shared blocklist adds a network dimension. When one site catches and reports a spam IP, every other site using the same blocklist benefits immediately.

The idea is simple in principle:

The value comes from scale. A bot IP that hit a dozen other sites before reaching yours is caught before it even gets to the honeypot. The higher the minimum severity you require, the fewer false positives, but the slower the protection against newly active IPs.

Minimum severity. A severity of 1 blocks any IP that has been reported once, anywhere. That is aggressive and risks false positives. A minimum of 2 or 3, requiring corroboration from multiple independent installs, is a safer default for most sites.

The optional capstone: AI triage

For the messages that pass all five layers, there is an optional further step.

Most spam is stopped by the cheap server-side layers: honeypot, time trap, rate limit, content heuristics, and blocklist. But some borderline submissions make it through. The message was composed slowly, from a fresh IP, with no obvious keyword hits, but something about it still reads as automated or off-topic.

For these borderline cases, you can optionally pass the message text to a language model with a short spam/ham classification prompt. The model reads the actual content and returns a verdict: deliver to inbox, or quarantine for review.

A few practical notes on this approach:

This is a forward-looking option for sites that receive a lot of sophisticated spam. For most contact forms, the five server-side layers alone are sufficient.

Putting it together: order and tuning

Run the cheapest checks first and log everything while you tune.

The order of checks matters. Put the ones with the lowest processing cost first, so that the majority of spam never reaches the more expensive steps:

  1. IP blocklist check (shared or local). A single database lookup. Reject immediately if the IP is listed.
  2. Honeypot check. A single field comparison. Costs almost nothing.
  3. Time trap. A timestamp subtraction. Costs almost nothing.
  4. Rate limit check. One database read and one write. Slightly more expensive, but still fast.
  5. Content heuristics. Keyword scan and link count across text fields. Moderate cost.
  6. AI triage (optional). One LLM API call. Only for borderline cases that passed everything above.

Log, do not silently drop

While tuning your thresholds, log rejected submissions with the reason. You cannot spot false positives in data you are throwing away.

Tune to your own spam

The keyword list and time threshold that work for one site may be wrong for another. Review your logs, add terms that keep appearing, and drop ones that are not showing up.

Revisit periodically

Bot tactics shift over time. A keyword list that cleared your inbox in January may be stale by June. A quick monthly review is enough.

On PageMotor: EP Email’s Spam Protection

If you build on PageMotor, these layers are already built in.

EP Email ships a Spam Protection settings group that implements all five layers described in this guide. Here is what each setting does:

SettingWhat it does
Enable HoneypotAdds a hidden trap field to every contact form. Any submission that fills it in is rejected. On by default.
Enable Time TrapRejects submissions that arrive faster than a real person could fill in the form. Catches bots that bypass the honeypot. On by default.
Minimum Fill Time (seconds)The threshold for the time trap. Default is 3 seconds. Raise to 5 for very short forms; lower to 2 only if real visitors report failures.
Enable Rate LimitingLimits submissions from the same IP address. On by default.
Rate Limit (minutes)The rolling window for rate limiting. Default is 5 minutes.
Maximum Links per MessageRejects submissions whose combined fields contain more than this many http(s):// links. Default is 2. Set to -1 to disable.
Spam Keyword BlocklistOne phrase per line. Case-insensitive match against all text fields. Pre-filled with common starters (bitcoin, crypto, casino, seo services, guest post, backlink, link building, rank higher, viagra, cialis). Tune to the spam you actually receive.
Enable Central BlocklistPulls shared threat intelligence from the ElmsPark network: IPs caught by other installs are blocked on yours, and vice versa. Requires a per-site token. Off by default.

EP Email also ships a Gibberish-Name Filter (not covered in the general guide above) that catches bot-generated random strings in name fields, such as WwmDtQvvtgDtOaUwucR, using checks for long consonant runs, low vowel ratio, and random capitalisation. Real names pass cleanly. It can auto-ban the submitting IP to the local blocklist when it triggers.

All settings are in EP Email’s admin panel under the Spam Protection group and the Central Blocklist group directly below it.

Troubleshooting

The things that trip people up, and the fix for each.

Real visitors are being rejected by the time trap

Lower the minimum fill time. Start at 2 seconds and work upwards until spam appears again. Also check whether your form is being auto-filled by a browser password manager or autocomplete extension, which can complete the form faster than a human typing. If autocomplete is the culprit, consider starting the timer only after the first keypress rather than on page load.

Spam is still getting through after enabling all layers

Check your logs to see which check is being passed. If the honeypot is being bypassed, the bot is reading the HTML carefully and skipping it: verify that the field is hidden by CSS and not by the hidden attribute. If content is getting through, add the offending terms to your keyword blocklist. If the same IP keeps appearing, add it to your local IP blocklist.

The rate limit is blocking the same person submitting a correction

A 5-minute window means someone who submits and then immediately wants to send a follow-up will be blocked. This is usually the right trade-off for a contact form. If you need to allow quick corrections, raise the rate limit window and accept a small increase in spam risk, or add a note on your thank-you page explaining that corrections can be sent by email.

The keyword blocklist is blocking legitimate enquiries

Review the blocklist and remove any term that is too broad. A term like link would block half of all real messages; a term like link building is specific enough to be safe. If a real enquiry is being blocked, the offending keyword is almost certainly in your blocklist: check the rejected-submissions log to see which rule fired.

The shared blocklist is blocking someone I know is legitimate

The sender’s IP may have been reported by another site, possibly because it is a shared office IP or a VPN exit node. Lower the minimum severity setting so only IPs with strong corroboration are blocked, or manually remove the IP from your local copy of the list. Contact the blocklist provider to dispute the report if the false positive recurs.

See also