In January 2024 an agency client came to us: “Our lead-gen form for a large bank started taking 1,500–2,500 submissions a day, all junk. 90% have real email addresses, but the names are nonsense, the phone numbers exist but never answer, the cities are picked at random from the dropdown. The bank's team spends four hours a day reviewing this garbage.”
Classic 2026 problem: after LLM agents got computer-use capabilities, spamming forms became easier than ever. The bot does not need to solve CAPTCHA — it just hands it off to GPT-4o. Good news: form-spam bots still get caught by five layers, none of which require CAPTCHA. Setup takes about an hour.
Five layers against form-spam, in effectiveness order: honeypot (catches ~50% of the laziest), time-to-submit (cuts agents that POST in 200ms), behavioral score (mousemove, focus, scroll), email validation (disposable domains), rate-limit by IP + fingerprint. CAPTCHA can be skipped entirely — in 2026 it gets defeated by GPT agents 87% of the time.
Why CAPTCHA stopped being a silver bullet
reCAPTCHA v2 (“pick all the buses”), reCAPTCHA v3 (invisible score), and hCaptcha are all solved by LLM agents today. GPT-4o with computer use looks at the screenshot, clicks the right squares, waits a couple seconds, and passes. Around 87% accuracy. Services like 2Captcha and CapSolver run the same LLMs in the background for $0.001 per solve.
Meanwhile CAPTCHA still drops human conversion by 5–15%. In the worst case you get fewer leads from people and roughly the same number from bots. Before adding CAPTCHA, check whether the five no-CAPTCHA layers are enough.
Layer 1. Honeypot — a hidden field only a bot would fill
The cheapest and most effective filter. Add a field with a plausible name (name="website" or name="address_2"), hidden visually:
<input type="text" name="website" tabindex="-1"
autocomplete="off"
style="position:absolute; left:-9999px; opacity:0; pointer-events:none">
Real users do not see or touch the field. Most bots — especially simple Puppeteer or curl scripts — fill every input. If website contains anything on submit, it is a bot. Silent reject.
Honeypot alone catches 40–55% of spam on lead-gen forms. Five minutes to implement.
The top honeypot mistake is forgetting autocomplete="off". Chrome aggressively autofills fields named email, name, address. If your honeypot is called email_secondary, Chrome will put an email in it and your real user will fail the check. Use names Chrome will not fill: website, company_url, fax.
Layer 2. Time-to-submit
Humans need time to fill a form. Even a short one (email + phone) takes 8–15 seconds. A bot fires POST 200–800 ms after page load. Big difference.
Implementation: store a timestamp on page load, check the delta on submit:
// In the page
document.getElementById('ts').value = Date.now();
// On the server
const elapsed = Date.now() - submittedTimestamp;
if (elapsed < 3000) {
return reject('too fast');
}
if (elapsed > 30 * 60 * 1000) {
return reject('session expired');
}
3 seconds at the bottom and 30 minutes at the top is a sensible bracket. A very slow submit usually means the tab was sitting open for hours and the timestamp is stale — not a valid session. Layer 2 catches another ~20% on top of layer 1.
Layer 3. Behavioral signals
Humans move the mouse, focus fields, sometimes type and re-type. Bots often do not. Collect signals in a local JS:
- Mouse movement on the page (
mousemove); - Touch events on mobile (
touchstart); - Manual focus on inputs (not via
field.focus()); - Real typing into inputs (
keydownwith realistic 80–300ms intervals); - Scroll on the page.
Sum these into one behavior_score from 0 to 100. Below 25 — almost certainly automation. 25–60 — grey zone (let it through, log it). Above 60 — real human.
The signal collection can be a one-line TDS widget:
<script src="https://api.tds.so/widget.js"
data-form-id="banking-leadgen"></script>
The widget adds a hidden tds_behavior field with the encrypted score, which the backend verifies through our API. Without the widget you can implement the same thing yourself — about 50 lines of code and one form field.
Layer 4. Email validation without sending mail
Most spammers use disposable email services (10minutemail, guerrillamail, tempmail) or carelessly type test@gmail.com, asdf@yahoo.com. Simple check:
- Maintain a list of ~3,000 disposable domains (open-source lists on GitHub, auto-update).
- Check the domain's MX record synchronously (5ms) — if there is none, the mailbox does not exist.
- Plausibility check on the local part — long digit runs, no vowels, length over 30 — flags.
Do not do an SMTP handshake against Gmail/Outlook — they will block your IP for “email enumeration” within an hour. MX + disposable list gets 90% of the value with no legal or technical risk.
Layer 5. Rate-limit by IP and fingerprint
The final layer. Every form should have hard limits:
- No more than 1 submit per minute per IP (Redis counter with TTL=60s).
- No more than 5 submits per hour per fingerprint (in case the bot rotates IPs through a residential proxy).
- No more than 50 submits per day per /24 subnet (in case a botnet sits on adjacent IPs).
Without rate-limit, your form is vulnerable to the simple attack: the bot changes payload and fires one submit per second across 8 parallel threads. One hour: 28,000 fake leads. With rate-limit — 60 per hour per IP, and the attacker's botnet quickly hits the ceiling.
Spam signatures in your logs
When the layers are working, characteristic patterns show up in Sentry/logs. Easy to audit:
# Bots on legit Chrome UA but no mousemove
form_rejected: reason=behavior_score_low score=8
form_rejected: reason=behavior_score_low score=12
form_rejected: reason=behavior_score_low score=4
# Bots on disposable email
form_rejected: reason=disposable_email domain=tempmail.com
# Scripts that bypass the UI and POST directly
form_rejected: reason=honeypot_filled value="https://<long-url>"
# Speed attack — many submits per minute per IP
form_rejected: reason=rate_limit ip=185.X.Y.Z count=14/min
If 95% of log entries hit the same reason — the other layers may be redundant. If the distribution is mixed — all five are pulling weight.
When to add CAPTCHA after all
CAPTCHA is justified in three cases:
- Financial forms — banking leads, payment data. The cost of a false negative ($100–1000 per missed scammer) is much higher than the cost of dropping a real user.
- Targeted attacks — a specific competitor or hater is intentionally farming your form, and five layers are not enough. Rare.
- Compliance — some regulators (PCI, certain financial licenses) require an interactive check.
Outside of these, five layers without CAPTCHA are enough. And your human users will not suffer through bus pictures.
Conclusion
Back to the client from the opening. In one hour we stacked: honeypot + time-to-submit + TDS widget with behavior_score + email validation + rate-limit. Twelve hours later junk submissions dropped from 2,000 to 80 per day. A week later — 20. No CAPTCHA.
If your form catches more than 50 spam submissions a day, work through these five layers in order. Below that, honeypot + email validation is usually enough. The key habit: instrument each layer separately in logs, so you can see what is actually catching what.