How to implement rate limiting in Node.js: algorithms and practical examples
Rate limiting protects Node.js APIs from abuse and overload. This guide explains fixed window, sliding window, and token bucket algorithms, shows how to add rate limiting to an Express API, and how to test it.
On this page
- What rate limiting is and why you need it
- The 429 Too Many Requests response
- Rate limiting algorithms
- Fixed window counter
- Sliding window log
- Sliding window counter
- Token bucket
- Building a fixed window rate limiter in Node.js
- Using a rate limiting library with Express
- Limiting a token bucket with a library
- Where to apply rate limiting
- Best practices
- FAQ
- What is rate limiting in Node.js?
- What is the difference between rate limiting and throttling?
- Which rate limiting algorithm should I use?
- Does rate limiting work with multiple Node.js instances?
Rate limiting controls how many requests a client can send to your API in a given amount of time. When a client exceeds its limit, the server responds with the HTTP status 429 Too Many Requests, optionally including a Retry-After header that tells the client how long to wait before trying again. Rate limiting protects your API from abuse, prevents overload, and keeps performance predictable for every legitimate user.
What rate limiting is and why you need it
In networking, rate limiting means controlling how many operations can be performed in a given period of time, usually to avoid overloading a system and causing performance degradation. A server might limit the number of requests it accepts from a single client in a time period, which not only optimizes overall performance but also mitigates attacks such as denial-of-service (DoS). Without rate limiting, a single misbehaving or malicious client can consume all of your API's resources and make it slow or unavailable for everyone else.
Rate limiting is commonly applied to public APIs, login and password-reset endpoints, and any route that triggers expensive work. It is often confused with throttling, which gradually slows a client down, and with debouncing, which coalesces rapid events. Rate limiting rejects requests outright once a ceiling is reached, which is the strictest of the three.
The 429 Too Many Requests response
When a client exceeds its allowed request rate, the server returns the HTTP status 429 Too Many Requests. According to RFC 6585, a Retry-After header may be included to indicate how long the client should wait before making a new request. The 429 status tells both the client and any tooling (caches, proxies, monitoring) that the request was deliberately refused because of rate limiting, not because the server is broken.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": "rate_limit_exceeded",
"message": "You have exceeded the limit of 100 requests per minute"
}
The client should read Retry-After and stop sending requests until that many seconds have passed, then retry. Well-behaved clients use this header to back off gracefully instead of hammering the API in a loop.
Rate limiting algorithms
There are several algorithms for deciding when to allow or block a request. Each trades simplicity, memory, and fairness differently.
Fixed window counter
The fixed window counter divides time into fixed buckets (for example, 100 requests per 60 seconds). Every request increments a counter for the current window; when the window expires, the counter resets. It is the simplest to implement and needs only one counter per key, but it has a known weakness: two consecutive windows are independent, so a client can make twice the limit in the instant around a window boundary. A burst of 100 requests at the end of one minute and 100 at the start of the next passes a 100-per-minute limit.
Sliding window log
The sliding window log records a timestamp for every request. To decide whether to allow a new request, the server counts how many timestamps fall inside the current moving window and rejects if the count exceeds the limit. It is the most accurate, because the window truly slides, but it stores every request timestamp, so memory grows with traffic. For a high-traffic endpoint this can become expensive.
Sliding window counter
The sliding window counter approximates a true rolling window with two integers: the count for the current window and the count for the previous one. It computes a weighted estimate of requests in the window by blending the two counters, giving smooth behavior that is close to a real sliding window at a small, constant memory cost. This makes it popular in production at scale, and it is what many library-based rate limiters use.
Token bucket
The token bucket models an explicit burst capacity plus a steady refill rate. A bucket holds up to capacity tokens; a token is added at a fixed rate, up to the capacity, and every request consumes one token. If the bucket is empty, the request is rejected. The token bucket allows short bursts (up to the full capacity) while enforcing a sustained average rate, which is ideal for APIs where clients legitimately burst, such as batch jobs or media streaming.
| Algorithm | Accuracy | Memory | Burst handling | Best for |
|---|---|---|---|---|
| Fixed window | Coarse — boundary bursts | Low | Weak (boundary double-burst) | Simple rate caps |
| Sliding window log | Exact | High (stores every request) | Strong | Low-traffic, strict accuracy |
| Sliding window counter | Near-exact | Low (two counters) | Strong | Production at scale |
| Token bucket | Average-rate | Low | Intended (bounded burst) | User-facing APIs, streaming |
A common rule of thumb: token bucket when clients need to burst, sliding window counter when you want smooth, fair behavior under steady load, and fixed window when you need the simplest possible implementation.
Building a fixed window rate limiter in Node.js
You do not need a library to understand the mechanics. Here is a minimal fixed window limiter implemented as plain Node.js middleware for an Express app. It tracks one counter per IP address.
const WINDOW_MS = 60 * 1000; // 60 seconds
const MAX_REQUESTS = 100;
const hits = new Map(); // ip -> { count, resetAt }
function rateLimiter(req, res, next) {
const ip = req.ip;
const now = Date.now();
let entry = hits.get(ip);
if (!entry || now >= entry.resetAt) {
entry = { count: 0, resetAt: now + WINDOW_MS };
hits.set(ip, entry);
}
entry.count += 1;
res.setHeader("X-RateLimit-Limit", String(MAX_REQUESTS));
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, MAX_REQUESTS - entry.count)));
if (entry.count > MAX_REQUESTS) {
res.setHeader("Retry-After", String(Math.ceil((entry.resetAt - now) / 1000)));
return res.status(429).json({ error: "Too Many Requests" });
}
next();
}
app.use(rateLimiter);
This example stores state in a JavaScript Map in the process's memory. It works for a single Node.js instance, but it will not work correctly if you run multiple instances behind a load balancer, because each instance keeps its own counter. In a clustered or multi-instance deployment, use a shared store such as Redis so every instance reads the same counters.
Using a rate limiting library with Express
For production, a maintained library saves you from re-implementing edge cases such as IPv6 subnet handling, reliable header output, and pluggable stores. express-rate-limit is a widely used middleware for Express. Install it, configure a limiter, and attach it to your app or to specific routes.
import { rateLimit } from "express-rate-limit";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // max 100 requests per window per IP
standardHeaders: "draft-8", // emit the Ratelimit header
legacyHeaders: false, // disable the older X-RateLimit- headers
});
app.use(limiter); // apply to the whole app
// or: app.use("/api/auth", limiter); // protect only sensitive routes
A shared store makes the limiter consistent across multiple instances:
import { RedisStore } from "rate-limit-redis";
// ... limiter = rateLimit({ store: new RedisStore(...), ... })
express-rate-limit supports custom stores like rate-limit-redis so the hit counts live in Redis instead of in a single process's memory. It also lets you customize the key generator (for example, keying on an authenticated user ID instead of the raw IP, which matters behind a proxy or NAT where many users share one IP).
Limiting a token bucket with a library
Libraries such as bottleneck and rate-limiter-flexible provide token bucket and other rate limiter classes that you can drive from your own code rather than as Express middleware. The general shape is: create a limiter with a capacity and a refill rate, then consume a token before doing work.
import { RateLimiterRedis } from "rate-limiter-flexible";
const limiter = new RateLimiterRedis({
storeClient: redisClient,
points: 100, // capacity (burst)
duration: 60, // refill per 60 seconds
});
async function handle(req, res) {
try {
await limiter.consume(req.ip);
// proceed with the request
} catch {
res.status(429).json({ error: "Too Many Requests", retryAfter: 60 });
}
}
The consume call either resolves (allow) or rejects (limit exceeded). You decide what to do on rejection, typically returning 429 with a Retry-After header.
Where to apply rate limiting
Rate limiting is not one-size-fits-all. Different routes need different limits, and keying on the right identifier matters.
- Sensitive routes (login, password reset, signup) should have aggressive limits — for example, a handful of attempts per minute per IP or per account — because they are prime targets for credential stuffing and brute force.
- Read-only public routes can allow much higher limits.
- Key by user identity, not raw IP, when possible. Behind a proxy, NAT, or CDN many real users share a single visible IP, so limiting purely by IP can block legitimate users. If requests are authenticated, key on the user ID or API key.
- Servers behind a CDN such as Cloudflare can enforce rate limiting at the edge, before traffic reaches your origin. This offloads protection and lets you set custom limits on specific paths — a good complement to in-application limiting.
Best practices
- Always return
429with aRetry-Afterheader so well-behaved clients back off automatically and you do not silently drop traffic. - Expose rate limit headers (
X-RateLimit-Limit,X-RateLimit-Remaining, or the newerRatelimitheader) so clients and tooling can predict and respect the limit. - Use a shared store for multi-instance deployments. In-memory counters are per-process and become inaccurate the moment you scale horizontally.
- Log and monitor
429responses. A spike in429s can reveal a bug in your client code, a misbehaving crawler, or an attack in progress. - Pick the key carefully. Limiting by IP is simple but coarse; limiting by authenticated user or API key is far more fair behind proxies.
- Do not rate limit only as an afterthought — decide your limits, store, and headers up front so clients are built to respect them from the start.
FAQ
What is rate limiting in Node.js?
Rate limiting in Node.js is the practice of controlling how many requests a single client can make to your server in a period of time. When a client exceeds its allowance, the server returns the HTTP status 429 Too Many Requests. It is implemented either with a dedicated library or with a custom middleware that tracks request counts per client key (usually IP address or user ID).
What is the difference between rate limiting and throttling?
Rate limiting rejects requests once a client passes a maximum count in a time window, returning 429. Throttling instead slows a client down by introducing delay, allowing the request eventually but at a reduced pace. Debouncing is a third related technique that coalesces rapid repeated events into fewer calls. Rate limiting is the strictest of the three.
Which rate limiting algorithm should I use?
Use a token bucket when clients legitimately need to burst, a sliding window counter when you want smooth, fair behavior under steady load, and a fixed window counter when you need the simplest possible implementation. Sliding window log is most accurate but stores every request timestamp, so it suits low-traffic endpoints only. For most production Node.js APIs, a sliding window counter or token bucket is a good default.
Does rate limiting work with multiple Node.js instances?
Only if you use a shared store. An in-memory counter is local to a single process, so if you run several instances behind a load balancer each one would track its own counts, and the effective limit would be multiplied by the number of instances. Libraries such as express-rate-limit and rate-limiter-flexible accept a shared store such as Redis to keep counts consistent across all instances.
Sources
- MDN Web Docs — 429 Too Many Requests (developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429
- CC-BY-SA 2.5)
- MDN Web Docs — Rate limit (developer.mozilla.org/en-US/docs/Glossary/Rate_limit
- CC-BY-SA 2.5)
- Wikipedia — Rate limiting (en.wikipedia.org/wiki/Rate_limiting
- CC BY-SA)
- IETF RFC 6585 — Additional HTTP Status Codes (iatf.org/rfc/rfc6585
- informational)
- Cloudflare Developers — Rate limiting rules (developers.cloudflare.com/waf/rate-limiting-rules
- docs reuse)
Share this guide
Related guides
- How to handle errors in Node.js: try/catch, error-first callbacks, promises, and event emitters
Node.js errors come in several asynchronous styles, and each has its own correct way to be caught. This guide explains try/catch for synchronous code, error-first callbacks, promise rejection handling, and the 'error' event for streams and event emitters.
- How to Use Environment Variables in Node.js: process.env, .env files, and secure secrets
Environment variables are the standard way to configure a Node.js app without hardcoding values. Node reads them through process.env, you can load them from .env files with the built-in --env-file flag or the dotenv package, and secrets should stay out of your code and version control. This guide walks through each approach and when to use it.
- How to add JWT authentication to a Node.js API: a practical guide
Step-by-step implementation of JWT authentication on a Node.js/Express API: password hashing, login endpoint, token verification middleware, refresh tokens, and the common security pitfalls.