How We Stopped Bot Signups and Contact Form Spam in Clarity
A production case study: the bot pattern we saw, the layered controls we implemented, and how we reduced abusive sign-up and contact traffic without hurting real users.
We recently saw a classic abuse pattern: gibberish contact submissions, low-quality sign-up attempts, and repeated traffic from suspicious IPs. This post explains exactly what we changed in Clarity to reduce bot traffic without blocking legitimate users.
The Pattern We Saw
The first signal was noisy form input that looked machine-generated rather than human-written. Then we noticed clusters of brand-new accounts with no confirmation activity, no real profile data, and no follow-up behavior. That combination usually means automated probing.
Important detail: bots do not only target login forms. They target every public input surface, including contact forms, waitlists, and social sign-up flows. If one endpoint is weak, it becomes the entry point.
Our Defensive Model: Friction by Risk Level
We did not want to punish normal users with unnecessary friction. So we used a layered model:
Cheap filters first (honeypot fields and basic input constraints).
Rate limits second (per-IP, burst, and daily windows).
Challenge checks third (Turnstile with server-side action validation).
Automated cleanup last (hourly sweep to ban stale bot-style accounts).
This keeps the fast path smooth for real users while making automated abuse progressively more expensive.
Layer 1: Honeypots and Quiet Failures
We added hidden honeypot fields to sign-up and contact flows. Legitimate users never fill these fields, but simple bots often do.
When tripped, we return a success-like response instead of a loud rejection. That matters, because explicit failure messages help attackers tune their scripts.
Layer 2: Durable Rate Limits for Public Endpoints
Rate limiting is obvious in theory and often weak in practice. We enforce limits across several dimensions instead of just one:
Per-IP hourly limits for contact submissions.
Sign-up burst limits (short window) to absorb bot spikes.
Sign-up daily IP limits to slow long-running abuse.
Email-level limits to prevent repeated hammering of one target identity.
The key is durable counters in the data layer, not only in-memory counters. In-memory limits reset on deploy and are easy to bypass across instances.
Layer 3: Turnstile With Server-Side Verification
Client-side widgets alone are not enough. We verify Turnstile tokens on the server and match expected action values (for example, sign-up vs contact form). If action does not match, we reject.
We also pass the request IP to verification and keep strict enforcement enabled in production. If a challenge token is missing or invalid, the request fails before sensitive auth operations run.
Layer 4: Automated Bot-Abuse Sweep
Even with prevention, some junk accounts still get created. So we added an hourly Vercel cron job that sweeps for high-confidence bot accounts and bans them automatically.
Our sweep criteria target accounts that are all of the following:
Unconfirmed.
Never signed in.
No corresponding app profile.
Older than a stale threshold (72 hours by default).
These accounts are banned by setting a long-term ban window, and the same sweep pass also cleans stale sign-up rate-limit rows.
Why Cron Security Matters Too
Cron routes are operationally sensitive. Ours are guarded by:
Bearer-token cron auth checks.
Scoped secret support and global fallback policy.
Distributed run locks to prevent duplicate concurrent sweeps.
Without cron auth and locking, an attacker can trigger expensive background jobs repeatedly or cause race conditions.
What We Did Not Do
We intentionally avoided a few common anti-patterns:
Blocking all disposable email providers (too many false positives).
Permanent IP bans at first sighting (unsafe for shared networks).
Only relying on CAPTCHA (insufficient as a single control).
Returning verbose security errors to public clients.
Operational Checklist You Can Reuse
If you are hardening a Next.js app, this is the sequence we recommend:
Add honeypot fields to every unauthenticated write endpoint.
Enforce input bounds early (length, format, required fields).
Add durable multi-window rate limits for sign-up and contact paths.
Use Turnstile or equivalent with strict server-side token verification.
Add an authenticated cron sweep for stale unconfirmed bot accounts.
Protect cron routes with scoped secrets and lock runs to avoid overlap.
Log outcomes in a way that supports investigation without leaking secrets.
Outcome
After these changes, bogus submissions dropped sharply and auth endpoints stopped surfacing spurious incidents from obvious bot traffic. The result is not "perfect security"; it is a safer baseline with automated hygiene and fewer manual interventions.
The biggest lesson: bot defense should not be a patch after incident response. It should be part of your default system design for any public app. Treat abuse handling as a product requirement, not an afterthought.