Rate Limiting in Node.js: Strategies That Actually Scale
A payment API that accepts every request is not a feature — it is a liability. Whether you are defending against a misconfigured mobile client hammering your endpoint, a competitor scraping your pricing, or a genuine DDoS attempt, rate limiting is the first line of control you have before traffic hits your business logic.
Most tutorials stop at dropping express-rate-limit into a middleware chain and calling it done. That works for a blog. It does not work for a fintech API processing mobile money callbacks, wallet top-ups, and OTP verifications simultaneously — each with wildly different burst characteristics.
This article breaks down four algorithms, shows real Node.js implementations, and explains which one to reach for depending on the traffic shape you are defending against.
Why the Algorithm Choice Actually Matters
Rate limiting is not just about rejecting requests. It is about how gracefully you reject them and how accurately you measure consumption. A poorly chosen algorithm either lets burst abuse slip through or punishes legitimate users during normal peak hours — both outcomes cost you money and trust.
African fintech APIs face a specific challenge: mobile network latency causes clients to retry aggressively. A user whose OTP request times out at the network layer will often fire the same request three or four times in under two seconds. A naive fixed-window counter will treat all four as violations. A well-tuned sliding window will see them for what they are.
Algorithm 1: Fixed Window
The simplest approach. You divide time into fixed buckets (e.g., one-minute windows) and count requests per bucket.
// Express middleware — fixed window using Redis
const redisClient = require('./redisClient');
function fixedWindowLimiter(limit, windowSeconds) {
return async (req, res, next) => {
const key = `fw:${req.ip}:${Math.floor(Date.now() / (windowSeconds * 1000))}`;
const count = await redisClient.incr(key);
if (count === 1) await redisClient.expire(key, windowSeconds);
if (count > limit) {
return res.status(429).json({ error: 'Too many requests. Please slow down.' });
}
next();
};
}
The problem: the boundary edge. A client can fire 100 requests at 11:59:59 and another 100 at 12:00:01 — effectively sending 200 requests in two seconds while technically staying within a per-minute limit. For low-value endpoints this is tolerable. For OTP or transaction endpoints, it is not.
Algorithm 2: Sliding Window Log
Instead of a fixed bucket, you track the exact timestamp of every request and count only those within the last N seconds relative to now.
This is the most accurate algorithm but also the most memory-intensive. Storing a timestamp per request per user in Redis is expensive at scale. Use it only for low-volume, high-sensitivity endpoints like login attempts or PIN resets where accuracy is non-negotiable.
When to use it: authentication endpoints, admin actions, anything where a two-second burst window could cause real damage.
Algorithm 3: Sliding Window Counter (Hybrid)
This is the sweet spot for most production APIs. It approximates the sliding window by blending two fixed-window counters — the current window and the previous one — weighted by how far into the current window you are.
estimated_count = prev_count × (1 - elapsed_ratio) + curr_count
You get near-sliding-window accuracy at fixed-window memory cost. Redis stores only two integers per user per endpoint, not a full timestamp log. This is the algorithm behind Cloudflare's rate limiter and it holds up well against the retry storms common on unstable mobile networks.
Algorithm 4: Leaky Bucket
The leaky bucket processes requests at a constant outflow rate regardless of how fast they arrive. Excess requests queue up; if the queue overflows, new arrivals are dropped.
This is ideal when you want smooth throughput rather than hard caps — for example, a bulk SMS dispatch service or a report generation queue. It prevents backend overload by decoupling ingress rate from processing rate.
The downside: it introduces latency for legitimate burst traffic. A user uploading a batch of 50 transactions at once will have those requests queued and drip-fed to your processor. If the client has a short timeout, those queued requests may expire before they are processed. For interactive APIs, this is often the wrong tradeoff.
Algorithm 5: Token Bucket
Token bucket gives each client a bucket that fills at a constant rate (e.g., 10 tokens per second, max 50). Each request consumes one token. If the bucket is empty, the request is rejected.
Unlike leaky bucket, token bucket allows controlled bursts — a client can accumulate tokens during idle time and spend them in a short burst. This matches how real users actually behave: idle for minutes, then suddenly active.
For a wallet dashboard API where users open the app, load their balance and transaction history in parallel, token bucket is a natural fit. The initial parallel load consumes several tokens at once, and the bucket refills for the next interaction.
Combining Algorithms Per Route
Production systems rarely use one algorithm globally. A more mature pattern is to apply different strategies per route class:
/auth/*and/otp/*— Sliding window log. Low volume, maximum accuracy, brute-force protection./transactions/*and/payments/*— Sliding window counter. High accuracy at scale./dashboard/*and/accounts/*— Token bucket. Handles parallel page-load bursts gracefully./reports/*and/exports/*— Leaky bucket. Smooth out expensive backend queries.
This multi-strategy approach requires a shared rate-limit layer (Redis is the standard choice) so limits are enforced consistently across all instances of your Node.js service, not per-process.
What to Return When You Reject
A 429 response should always include the Retry-After header and, ideally, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. This allows well-behaved clients to back off intelligently rather than retrying immediately and compounding the load.
Do not return a generic 500 or a vague error message. Clients — especially mobile SDKs — will retry on 500. They will not retry on 429 if they are implemented correctly.
Operational Considerations
- Redis key TTL discipline: Always set expiry on rate-limit keys. A missing
EXPIREcall is a memory leak waiting to happen. - Bypass lists: Internal services, health checks, and trusted partners should bypass rate limiting at the middleware level, not by inflating their limits.
- Observability: Emit a metric every time a 429 is issued. A sudden spike in rate-limit hits is often your first signal of a client bug or an emerging abuse pattern — before it becomes an incident.
Why This Matters for Your Project
If you are building or scaling a Node.js API — whether it is a payments platform, a logistics service, or a multi-tenant SaaS product — rate limiting is not an afterthought you bolt on before launch. The algorithm you choose shapes your API's resilience under real-world traffic, the experience of legitimate users during peak load, and your exposure to abuse. Picking the right strategy per endpoint, implementing it against a shared Redis store, and instrumenting it properly is the difference between an API that holds up and one that becomes a support ticket at the worst possible moment.




