Skip to main content
Back to Blog
7 min read

I deleted rate limiting from my contact form. Here's why.

Rate limiting is a reflex. On a real portfolio contact form it solved nothing, added a Redis dependency, and broke legitimate submissions. The honest math on when to skip it.

securitynext.jstradeoffs

Every Next.js contact-form tutorial includes rate limiting. Most recommend Upstash Redis with a sliding window, 3 submissions per minute per IP. I shipped that pattern, ran it for a month, and then deleted it.

This is a companion piece to the four-line spam filter that replaced reCAPTCHA on this site. Same contact form. One more simplification.

What I had

The canonical "secure contact form" stack:

Roughly 80 lines of code plus a Redis dependency plus a Cloudflare worker. For a contact form that gets ~5 submissions a week.

What I noticed after a month

Three data points made me reconsider.

1. The rate limiter never triggered on a real attack.

The honeypot and time-floor caught every spam attempt I could verify. I grepped logs for "rate limit exceeded" events — zero, in 31 days. The bots that tried to submit were all failing the honeypot or the time-floor before hitting the rate limit. The rate limiter was downstream of the filters that actually worked.

2. It did trigger on legitimate users twice.

Both times were humans on corporate networks — large office, shared IP, one person submitting for themselves, another person submitting right after. Second submission hit the 3/min limit. They got a polite "please try again later" response and, probably, didn't.

Two lost leads on a form that sees ~20 submissions a month is a 10% false-positive rate on the thing that's supposed to catch bots. That's worse than not rate limiting.

3. Redis added a failure mode.

Upstash is excellent. But it's another dependency, another API call per submission, another place that can be slow or unavailable. Twice during the month the Upstash response was >500ms, which made the submission feel sluggish. One time (briefly) Upstash returned an error, which my code handled by failing open — the rate limit didn't apply that minute. At which point: what was it really defending?

The math on when rate limiting pays off

Rate limiting is genuinely valuable when:

Rate limiting is a reflex-without-payoff when:

What I replaced it with

Nothing. The filter chain is now:

  1. Cloudflare at the edge (set-and-forget)
  2. Honeypot
  3. Time-floor (1500ms minimum)
  4. Size limits (name < 100, email < 200, message < 5000)
  5. Telegram notification

That's the whole pipeline. No Redis, no state, no sliding window. 60ms p95 response time.

The worst case I might be wrong about

A motivated attacker could, in theory, script a headless browser that mounts the page, waits 1600ms, fills the form, and submits — bypassing the time-floor. Now I have a bot that submits 100 times a minute.

What actually happens then:

The key insight: rate limiting is a fix for a specific attack shape. I can add it back in under an hour when I see the attack. Paying for it preemptively, forever, on a form that has never needed it, is a tax against a threat that hasn't materialized.

The generalized rule

I used to add every security middleware from the top tutorial. Now I ask three questions before adding one:

  1. What specific attack does this defend against? If I can't name it in one sentence, I don't need the defense.
  2. What does it cost — in code complexity, in dependencies, in latency, in false positives? Each defense is a tax.
  3. Can I add it later when the attack actually appears? For most defenses on small-traffic systems, the answer is yes — and preemptive defense is overspending.

Rate limiting on a contact form failed all three for me:

  1. The attack it defends against is mostly caught by cheaper, upstream filters.
  2. It added Redis, ~20 lines of code, and 2 false positives per 20 submissions.
  3. I could absolutely add it back in an hour when (if) an actual attacker shows up.

Gone. Not missed.

The caveat

This works because the form is mine, the volume is small, and the downstream action (Telegram notification to me) has no public side-effects. If you're building:

— keep your rate limiter. It exists for a reason, and the reason is your specific shape.

This post is specifically about low-traffic, owner-viewed, side-effect-free form endpoints. The internet mostly writes defensive-by-default guides, which is right for the median project and wrong for specific ones. Knowing when "specific" applies to your project is how you stop paying for defenses you don't need.

The honest trade I'm making

I'm trading a defense-in-depth against an attack that hasn't happened, for a simpler codebase and fewer dependencies. That trade is right for this form, today. It would be wrong for a different form tomorrow.

The failure mode, if I'm wrong: a weekend of spam in Telegram and fifteen minutes of code revert. I can live with that.

The failure mode if I'd kept the rate limiter: two lost real conversations per month, forever, silently. I can't.


If you're reviewing your own security stack and want a second opinion on what to keep versus delete, I'll read through your threat model and respond with one-page of honest feedback. Not trying to sell an audit — half the time my answer is "you're overspending." Contact form.