DEV Community

Cover image for Your Rate Limiter Works Until You Deploy a Second Server

Your Rate Limiter Works Until You Deploy a Second Server

Amin Sadeghi on October 24, 2026

A rate limiter can work perfectly in development and still fail in production. You test it locally, send too many requests, and receive a 429 Too ...
Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

The shared-counter point is the one that matters, and the failure I would add is on the key, not the store: keying by IP works until a whole office or a mobile carrier's CGNAT shares one address, and then one heavy user eats the quota for everyone behind it. I learned to key authenticated routes by user ID and leave IP-keying for anonymous endpoints only, with per-endpoint limits, because a login route and a public read route never deserved the same budget. The test that caught it for me was not the single-request check but firing concurrent bursts through two instances and watching whether every server agreed on the count. Do you key your authenticated routes by user, or does everything still hang off the IP?

Collapse
 
aydope profile image
Amin Sadeghi •

That’s a good point about CGNAT. I’d also avoid treating the IP as the only identity signal, especially on authenticated routes. Combining user-based limits with IP-based limits seems like a more reliable approach, since each catches a different kind of abuse

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Combining the two is the right instinct, but I would keep the buckets separate rather than stacking them on the same request. In practice I have seen a logged-in user behind CGNAT hit the shared IP cap even though their own account was well under its limit, so the IP rule punished the wrong person. My rule now is: IP limits for anonymous traffic, user limits for authenticated traffic, and the IP limit only as a much higher backstop for authenticated calls. That way a stolen token still cannot hammer the API from a new network, but a normal user is not throttled by their neighbors. Do you check the user bucket first and skip the IP bucket on success, or do you run both and take the stricter result?

Thread Thread
 
aydope profile image
Amin Sadeghi •

I’d run both, but with different thresholds. The user bucket should handle normal authenticated usage, while the IP bucket acts as a higher safety ceiling against abuse. That keeps shared IPs from causing unnecessary throttling without leaving room for a single IP to flood the API

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

two thresholds is the clean split. user bucket for normal use, ip ceiling for abuse, no shared-ip collateral.

Thread Thread
 
aydope profile image
Amin Sadeghi •

Exactly. The key is making the IP limit a safety net, not a bottleneck for legitimate users behind shared IPs

Collapse
 
launchgatecheck profile image
Launch Gate •

Redis sharing state fixes the split-brain, but watch the increment itself: read-then-write from two instances can both pass under the limit. Use an atomic INCR with an EXPIRE set only on the first increment (or a Lua script), so the window and the counter live or die together. Two more production cases: fixed windows let a client fire 2x the limit at a window boundary, so sliding window logs or a token bucket are worth the extra Redis work on login endpoints. And decide what Redis being down means - fail open and the limiter vanishes during an outage, fail closed and your login page goes down with the cache. For most apps, fail open plus an alert on the limiter being unreachable is the survivable choice.

Collapse
 
aydope profile image
Amin Sadeghi •

Exactly! The Redis outage part is easy to overlook. Rate limiting shouldn’t become the reason your entire login flow goes down. I’d probably go with fail-open plus proper alerting, while keeping an eye on how much risk that introduces for the specific endpoint