This is the form your visitors will see
Clean, fast, no CAPTCHA. Try it — this is a real working demo.
What happens next ⚡
- 📥Submission received — stored in your searchable dashboard
- 📧Email notification — delivered to your inbox instantly
- 🛡️Spam filtered — AI classifier + honeypot, no CAPTCHA
- 🔗Webhook fired — optional: forward to Slack, Discord, Sheets
- ↩️User redirected — to your thank-you page
500 submissions/month free · No credit card
Why you need rate limiting on forms
Without rate limiting, a single attacker can submit your contact form 1,000 times per minute. Each submission sends you an email, fills your database, and burns your email provider quota. Within an hour, your inbox is unusable and your form is effectively broken.
Rate limiting prevents this by capping submissions per source. The three problems it solves:
- Spam floods — bots submitting hundreds of forms per minute, filling your inbox and dashboard with junk.
- Resource exhaustion — attackers intentionally exhaust your email quota (e.g. 10,000 emails/day on SES) to prevent legitimate notifications from going through.
- Harassment — a malicious user repeatedly submitting threatening or abusive content from one IP.
Rate limiting is the most cost-effective form security measure — it costs nothing to implement and stops the highest-volume attacks.
For the broader security picture, see contact form security.
Types of rate limits: per-IP, per-form, global
Three layers of rate limiting, each protecting against a different attack pattern:
- Per-IP rate limit — caps submissions from a single IP address. Stops individual attackers and single-source bots. Example: 5 submissions/hour per IP.
- Per-form rate limit — caps total submissions to a specific form across all IPs. Protects a high-traffic landing page form from being overwhelmed. Example: 100 submissions/hour per form.
- Global rate limit — caps total submissions across all forms on your account. Protects your overall email quota and server resources. Example: 500 submissions/hour total.
Per-IP is the most important — it stops the actual attacker. Per-form and global limits are safety nets for overall system health.
Implementation: Express.js with express-rate-limit
The most popular rate limiting library for Node.js. It uses an in-memory store by default (fine for single-server setups) and supports Redis for distributed deployments:
import rateLimit from 'express-rate-limit';
const formLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 submissions per IP per hour
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
message: {
success: false,
message: 'Too many submissions from this IP. Try again later.',
},
// Skip successful GET requests (only count POSTs)
skip: (req) => req.method !== 'POST',
});
// Apply to your form endpoint
app.post('/api/contact', formLimiter, (req, res) => {
// Handle form submission
res.json({ success: true });
});For multi-server deployments, use the Redis store so all servers share rate limit state:
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
const formLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }),
windowMs: 60 * 60 * 1000,
max: 5,
});Implementation: Next.js App Router
For Next.js API routes (App Router), use the same express-rate-limit pattern adapted for the Web Fetch API, or use a lightweight in-memory Map:
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX = 5;
const submissions = new Map<string, number[]>();
function rateLimit(ip: string): boolean {
const now = Date.now();
const timestamps = submissions.get(ip) ?? [];
const recent = timestamps.filter(t => now - t < RATE_LIMIT_WINDOW);
if (recent.length >= RATE_LIMIT_MAX) {
return false; // Rate limited
}
recent.push(now);
submissions.set(ip, recent);
return true; // Allowed
}
export async function POST(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
if (!rateLimit(ip)) {
return NextResponse.json(
{ success: false, message: 'Rate limited. Try again later.' },
{ status: 429 }
);
}
// Process form submission...
return NextResponse.json({ success: true });
}Note: in-memory rate limiting doesn't persist across serverless cold starts. For production on Vercel, use Upstash Redis (serverless Redis with a free tier) to share rate limit state across invocations.
Implementation: Nginx rate limiting
If you're behind Nginx (or using Nginx as a reverse proxy), you can rate limit at the network layer before the request even hits your application:
# nginx.conf
# Define a zone: 10MB stores ~160,000 IP addresses
limit_req_zone $binary_remote_addr zone=form_limit:10m rate=5r/h;
server {
location /api/contact {
# Burst allows 2 extra requests above the rate
# nodelay means burst requests are rejected immediately
limit_req zone=form_limit burst=2 nodelay;
limit_req_status 429;
proxy_pass http://localhost:3000;
}
}Nginx rate limiting is the fastest option — the request never reaches your application server if it's over the limit. Combine with application-level rate limiting for finer control (e.g. different limits per form).
Cloudflare edge rate limiting (zero code)
If your site is behind Cloudflare, you can set up rate limiting rules in the dashboard — no code changes needed:
- Go to Cloudflare Dashboard → Security → WAF → Rate limiting rules.
- Create a rule: if URL path matches
/api/contactand method is POST, allow max 5 requests per hour per IP. - Action: Block (returns 429).
Cloudflare rate limiting is free on all plans and runs at the edge — attackers are blocked before they reach your origin server. This is the fastest and cheapest option for sites already using Cloudflare.
Recommended rate limit values for contact forms
Based on production data across millions of form submissions on splitforms:
- Per-IP per hour: 5 submissions. Real humans rarely submit more than once. A limit of 5/hour allows for accidental re-submissions and shared NAT without blocking legitimate users.
- Per-IP per day: 20 submissions. Caps daily abuse from a single source.
- Per-form per hour: 100 submissions. Protects against distributed attacks (many IPs hitting one form).
- Global per hour: 500 submissions. Protects your email quota and server resources.
Start strict. If legitimate users complain about being rate limited (rare with these values), loosen slightly. Track 429 responses in your analytics to detect when limits are being hit and by whom.
Returning proper HTTP 429 responses
When rate limiting kicks in, return a 429 Too Many Requests status with helpful headers:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 3600 // seconds until the limit resets
X-RateLimit-Limit: 5 // total allowed per window
X-RateLimit-Remaining: 0 // remaining in current window
X-RateLimit-Reset: 1719753600 // Unix timestamp when window resets
{
"success": false,
"message": "Rate limited. Please try again in 55 minutes."
}The Retry-After header is standardized in RFC 7231 and respected by all HTTP clients. Include it so programmatic submitters know when to retry.
Rate limiting on splitforms
Splitforms applies rate limiting automatically on every plan — no configuration needed:
- Free (500/mo): 5 submissions per IP per hour, 50 per day. Built-in honeypot and AI spam filtering.
- Starter ($1/mo): Same rate limits, plus webhooks for custom automation.
- Pro ($5/mo): Custom rate limits, custom SMTP, and per-form rate limit configuration.
Rate limiting is one layer of a complete form security strategy. Combine it with a honeypot, AI spam filtering, and HTTPS for full protection. See contact form security for the complete guide.
Ready to protect your form? Get your free splitforms access key. Questions? Email hello@splitforms.com.
FAQ
What is form submission rate limiting?
Rate limiting caps how many form submissions a single IP address (or user) can send within a time window. For example: max 5 submissions per IP per hour, 20 per day. This prevents spam floods, resource exhaustion, and harassment. Rate limits are enforced server-side — the server tracks IP addresses and timestamps, rejecting submissions that exceed the threshold.
How many form submissions per hour should I allow?
For a typical contact form, 5 submissions per IP per hour and 20 per day is generous — a real human rarely submits more than once. For lead capture forms on high-traffic landing pages, you might set a higher per-IP limit (10/hour) but add a global cap (1000/hour for all IPs combined) to protect server resources. Start strict and loosen if you get legitimate complaints.
How is rate limiting different from a CAPTCHA?
CAPTCHAs verify that the submitter is human (by solving a visual or behavioral challenge). Rate limiting caps how many submissions come from one source regardless of whether they're human. They complement each other: rate limiting stops volume-based attacks (spam floods), while CAPTCHAs stop individual bot submissions. A honeypot + rate limiting is often enough without a CAPTCHA.
Should I rate limit by IP address or by user account?
Both, in layers. IP-based rate limiting catches anonymous bots and attackers. User-based rate limiting catches authenticated abuse (a logged-in user spamming your support form). For public contact forms, IP-based is the primary defense. Be aware that IP-based limits can affect users behind shared NAT (corporate offices, universities) — set per-IP limits generously enough to avoid blocking legitimate users on shared IPs.
Does splitforms handle rate limiting automatically?
Yes. Splitforms applies rate limiting on all plans — Free, Starter, and Pro. Default limits: 5 submissions per IP per hour, 50 per day. Pro plans ($5/month) allow custom rate limit configuration. If you're running your own server, you need to implement rate limiting yourself using express-rate-limit, django-ratelimit, Redis, or a CDN like Cloudflare.
More reads: contact form security, stop contact form spam, stop direct-to-API spam, all posts.