API Rate Limiting Strategies for Node.js
Fixed windows, sliding windows and token buckets — and why your rate limiter must live in Redis once you run more than one instance.
Rate limiting is one of those features that seems optional right up until the week it is not: a scraper hammers your search endpoint, a partner's retry loop goes exponential in the wrong direction, or a credential-stuffing run hits your login route at four hundred requests a second. Limits are cheap to add early and painful to retrofit under fire, so let us look at the strategies worth knowing and how to implement them properly in Node.
The three algorithms that matter
- Fixed window: count requests per clock window, e.g. 100 per minute. Trivial to implement, but allows bursts of up to double the limit across a window boundary.
- Sliding window: weights the previous window's count into the current one, smoothing the boundary problem for one extra Redis field.
- Token bucket: tokens refill at a steady rate and each request spends one, allowing short bursts while enforcing a long-term average. Best fit for public APIs with legitimate bursty clients.
For most internal and product APIs, a sliding window is the sweet spot of accuracy and simplicity. Reserve token buckets for published APIs where you document burst allowances to partners.
In-memory counters lie to you
The classic mistake is a Map of counters inside the process. It works perfectly in development, then production runs four instances behind a load balancer and every client quietly receives four times the intended limit — and a fresh allowance every deploy. Any limiter that has to mean something must keep its state in shared storage, which in practice means Redis.
import { RateLimiterRedis } from 'rate-limiter-flexible';
const loginLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:login',
points: 5, // attempts
duration: 900, // per 15 minutes
blockDuration: 900,
});
await loginLimiter.consume(email + ':' + req.ip);rate-limiter-flexible does the Lua scripting for you, so the check-and-increment is atomic even under heavy concurrency. Note the compound key on the login limiter: limiting by IP alone punishes offices behind one NAT address, while limiting by account alone lets a distributed attack rotate accounts freely. Use both.
Identifying the client correctly is its own trap. Behind a load balancer, req.ip is the balancer's address unless you configure trust proxy — and once you do, remember that X-Forwarded-For is client-controlled beyond the hops you trust, so an attacker can rotate identities by forging the header if you naively read its first entry. Take the address your trusted proxy appended, not the one the client offered, and prefer keying authenticated routes on the account rather than the network.
Layer your limits
One global limit is a blunt instrument. In practice we deploy tiers: a generous per-IP ceiling at the edge to absorb outright abuse, a per-user limit on authenticated routes tied to plan level, and tight route-specific limits on expensive or sensitive endpoints — login, password reset, search, exports. The login limiter above is deliberately harsh because its cost asymmetry is extreme: five failures per quarter-hour inconveniences no genuine user but ruins credential stuffing.
Fail open, and tell clients the rules
Two operational details separate polished implementations from irritating ones. First, decide what happens when Redis is unreachable: for most products the right answer is to allow the request and alert loudly, because taking the whole API down with the limiter is a self-inflicted outage. Second, return the standard headers — RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and a Retry-After on 429 responses — so well-behaved clients can back off without guesswork.
Rate limiting will not make headlines in your changelog, but it is load-bearing infrastructure for both security and stability. An afternoon with Redis now saves an incident channel later.
If your API needs hardening before a launch, STRCLI can help — we do this for clients regularly.