The checklist
Reject unsupported methods and content types
Cheaply drops scanners before parsing body data.
Apply per-IP rate limits
Stops repeated scripted POSTs and gives you an abuse signal.
Enforce allowed domains when configured
Prevents copied access keys from being replayed on unrelated sites.
Validate honeypot fields server-side
Keeps the field useful even when bots bypass JavaScript.
Check form_loaded_at
Blocks impossible instant submits and stale replay attempts.
Verify CAPTCHA tokens only when enabled
Keeps normal forms frictionless and protected forms stricter.
Silently drop failures
Keeps bot feedback loops unhelpful.
Log enough to debug
Record gate name and request metadata without storing sensitive payloads forever.
Implementation notes
The order matters. Run the cheapest checks before parsing large multipart bodies or calling third-party CAPTCHA APIs. Rate limiting should happen early. CAPTCHA verification should happen late and only for forms that have a secret key configured.
Settings should be read live per request. If a customer adds example.com to allowed domains, enforcement should start immediately. If they turn honeypot off, the honeypot gate should skip immediately. Cached settings are fine only with a short TTL and save-time invalidation.
Rate-limit storage
A single-instance app can use an in-memory Map for a first pass. The moment you move to multiple regions, serverless, or horizontal scaling, move that counter to Redis. Upstash Redis is a common fit because each edge/serverless worker sees the same counter.
How this maps to splitforms
splitforms applies these checks on /api/submit before delivery. The form dashboard controls honeypot and allowed domain settings, while optional reCAPTCHA remains opt-in. You can test a normal form on the form action tester or read the full comparison in direct-to-API spam protection.
FAQ
What checks should run server-side on a contact form?
At minimum: method/content-type validation, rate limiting, server-validated honeypot, time-to-submit check, and optional CAPTCHA token verification. If the form owner configured allowed domains, enforce Origin or Referer too.
Should form spam protection use Redis?
Use Redis or another shared store if the app runs across multiple server instances or serverless regions. An in-memory map is fine only for a single long-lived instance.
What should happen on a spam rejection?
Return a benign 200 success response and do not deliver the submission. Silent drops avoid teaching bots how to adapt.