noted.do
The Tech Guy

Your App Needs a Speed Limit

Every endpoint on your app is an open invitation to send requests. Most visitors send a few. Some scripts will send as many as your server can take. Rate limiting is just the rule that says "this many, this fast, and no more." It's one of those features nobody notices when it's there and everybody notices when it isn't.

Here's what it protects you from.

Brute force on your login page. Without a limit, an attacker can try password after password as fast as your server answers. Even with good password hashing, you don't want to give them unlimited attempts.

Abuse of expensive actions. Sending a password reset email, a signup confirmation, an SMS code. Each of those costs you something, whether that's money, sender reputation or both. Someone hammering your "resend code" button can burn through your email quota or get your domain flagged as spam.

Scrapers and runaway clients. Sometimes it's not malicious at all. A buggy script with a retry loop, or a mobile app that refreshes too often, can quietly eat your server. A limit turns that into a clear error instead of an outage.

Plain fairness on APIs. If you offer an API, one heavy user shouldn't be able to slow it down for everyone else.

How it works is mostly counting. You pick a key that identifies who's making the request (an IP address, a user ID, an API key), count their requests over some window of time, and reject them once they pass the threshold. There are a few common ways to count.

A fixed window is the simplest. "Five attempts per minute," with the counter resetting at the top of every minute. Easy to build, but someone can squeeze in five at 0:59 and five more at 1:00.

A sliding window smooths that out by always looking at the last sixty seconds, whatever the current time is.

A token bucket is the one you'll see most in real systems. Each client has a bucket that fills with tokens at a steady rate up to some maximum. Each request spends a token. That allows short bursts (a page that loads several things at once) while still capping the long-run rate.

You don't have to implement any of these yourself if you already have a reverse proxy. Nginx has it built in. This limits a login endpoint to roughly five requests per minute per IP, with a small allowance for bursts:

# in the http block
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

# in your server block
location = /login {
    limit_req zone=login burst=5 nodelay;
    limit_req_status 429;
    proxy_pass http://127.0.0.1:3000;
}

The 10m is shared memory for tracking clients, which is plenty for a small site. Status 429 means "Too Many Requests," which is the correct HTTP response here. It's nice to include a Retry-After header too, so well-behaved clients know when to try again.

Caddy and Traefik have rate limiting available as well (Caddy through a plugin), and most frameworks have middleware for it. If you're running several app servers, keep the counters somewhere shared, Redis being the usual choice, or each server will count separately and the limit won't mean much.

Now the part where it gets less tidy.

IP addresses aren't people. A whole office, a school, or a mobile carrier's network can share one public IP, so a tight per-IP limit can lock out a crowd of legitimate users. And attackers with lots of IPs can spread their attempts thin. For a login page, the better approach is limiting on both: a looser per-IP limit plus a stricter limit per account. That stops someone from hammering one account from many places, without punishing everyone behind the same router.

If you're behind a proxy or a CDN, make sure the limit is using the visitor's real IP, not the proxy's. Otherwise everyone looks like the same client and you'll rate-limit your entire user base at once.

Think about what happens when a limit kicks in. For a login, a short delay or a temporary block is reasonable. Permanently locking an account after a few failed attempts sounds secure but hands attackers an easy way to lock out anyone whose username they know. Prefer slowing down over shutting the door.

And don't treat rate limiting as your only defense. It buys time and reduces noise. It doesn't fix a weak password policy or a slow endpoint.

My advice: start with your login, password reset, signup and any endpoint that sends email or SMS. Those are the ones that hurt when abused, and limiting them takes a few lines of config. Add more as you see traffic that needs it.

Comments

Be kind. See the rules.