· rate-limiting / redis / nodejs
Best rate-limit library for Node.js + Redis in 2026
rate-limiter-flexible is the default for Node.js APIs on Redis. @upstash/ratelimit is the only serverless edge option. Here is how to choose.
By Ethan
1,848 words · 10 min read
Use rate-limiter-flexible v11.1.0 for any Node.js API running on your own infrastructure. Use @upstash/ratelimit v2.0.8 if you deploy to Vercel Edge Functions, Cloudflare Workers, or AWS Lambda. If your team is already on express-rate-limit, add rate-limit-redis v5.0.0 as the Redis store — five extra lines, no new abstractions.
Who this is for
Node.js developers who need distributed rate limiting: multiple app instances behind a load balancer, or serverless functions where per-process memory counters are meaningless. If you are running a single process and a restart can reset the counter without business impact, an in-memory store is fine and this article is not for you.
What we tested
All version numbers and download counts are pinned to 2026-06-01:
rate-limiter-flexiblev11.1.0 — ~1.2–2M weekly downloads, 3,530 GitHub stars, ISCexpress-rate-limitv8.5.2 +rate-limit-redisv5.0.0 — ERL: ~24–38M weekly downloads (includes all users, not just Redis users), MIT@upstash/ratelimitv2.0.8 — ~357K weekly downloads, MITbottleneckv2.19.5 — reviewed for completeness; not recommended for new projects- DIY
ioredisv5 + Lua scripts — the zero-dependency escape hatch
No rigorous third-party benchmark comparing all four libraries head-to-head exists as of this writing. One DEV Community post claims a 9× speedup for a custom implementation, but the methodology is not reproduced anywhere. We note the absence rather than fabricate numbers.
Why Redis
An in-process counter on instance A knows nothing about requests on instance B. Put two Node.js servers behind a load balancer and your rate limit doubles for free — the wrong kind of free.
Redis fixes this with three primitives. Shared state: every app instance reads and writes the same counter. Atomic Lua scripts: Redis executes Lua single-threaded, so two concurrent processes cannot both read count=4 and both increment to 5. TTL-based cleanup: EXPIRE keys vanish automatically, no cron required. The Lua EVAL approach beats WATCH/MULTI/EXEC retries under high contention because it needs one round trip and supports conditional logic; the Redis.io rate-limiting tutorial recommends it explicitly.
If you are also evaluating Redis against its open-source fork, see Redis vs Valkey 2026 for a breakdown of licensing, costs, and cluster gaps.
The contenders
rate-limiter-flexible — most complete library
Framework-agnostic. Works with any HTTP framework (or no framework). Uses Lua scripts internally so it is cluster-safe by default. Supports ioredis (the default) and node-redis v4+ (requires useRedisPackage: true — without the flag it assumes ioredis). Also supports MongoDB, DynamoDB, PostgreSQL, and Memcached with no API changes — useful if you want to degrade gracefully when Redis is unavailable.
v10.0.0 breaking change (March 2026): points and duration are now required with no defaults. Upgrading from v9.x without adding explicit values throws at startup.
Node-redis v4 cluster note: v4.7.0 carries two cluster mode bugs fixed in v5 — a race condition in cluster-slots discovery and an error-handling flaw in createCluster. For Redis Cluster deployments, use ioredis v5 or node-redis v5+.
The official docs report 0.7ms average in Node.js Cluster mode and 2.5ms in distributed mode. This is a self-reported benchmark; treat it as a floor, not a guarantee.
// ioredis setup (recommended for cluster)
const Redis = require('ioredis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = new Redis({ enableOfflineQueue: false });
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl',
points: 100, // requests allowed
duration: 60, // per 60 seconds
blockDuration: 600, // block 10 min after limit exceeded
});
module.exports = async function rateLimitMiddleware(req, res, next) {
try {
const result = await rateLimiter.consume(req.ip);
res.set({
'X-RateLimit-Limit': 100,
'X-RateLimit-Remaining': result.remainingPoints,
'X-RateLimit-Reset': new Date(Date.now() + result.msBeforeNext).toISOString(),
});
next();
} catch (rejRes) {
res.set('Retry-After', String(Math.ceil(rejRes.msBeforeNext / 1000)));
res.status(429).json({ error: 'Too Many Requests' });
}
};
If you are on node-redis v4+, add useRedisPackage: true to the constructor options and replace new Redis() with await createClient({ enable_offline_queue: false }).connect().
express-rate-limit + rate-limit-redis — Express default
The dominant rate limiter in the Express ecosystem, included by default in most scaffolding and boilerplate. It uses an in-memory MemoryStore unless you swap in a store; [email protected] is the official Redis store.
The algorithm is fixed window only — no sliding window, no token bucket. That is acceptable for most API protection scenarios but not for auth endpoints where boundary-burst attacks matter.
The 24–38M weekly download figure is for express-rate-limit total. Most of those installs use the in-memory store. Redis-backed installs are a subset.
[email protected] requires express-rate-limit >= 8.5.0. Confirm your ERL version before upgrading.
import express from 'express';
import { rateLimit } from 'express-rate-limit';
import { RedisStore } from 'rate-limit-redis';
import { createClient } from 'redis';
const app = express();
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15-minute window
limit: 100,
standardHeaders: 'draft-8',
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args: string[]) => redisClient.sendCommand(args),
}),
handler: (req, res) => {
res.status(429).json({ error: 'Too many requests, try again later.' });
},
});
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
limit: 5,
store: new RedisStore({
prefix: 'rl:auth:',
sendCommand: (...args: string[]) => redisClient.sendCommand(args),
}),
});
app.use('/api/', apiLimiter);
app.use('/auth/login', authLimiter);
@upstash/ratelimit — the only serverless-compatible option
Every other library in this list requires a persistent TCP connection to Redis. Cloudflare Workers, Vercel Edge Functions, and AWS Lambda do not support persistent connections in the same way a traditional server does. @upstash/ratelimit uses Upstash Redis, which exposes Redis over HTTP (REST). No persistent connection needed — the library works identically in Node.js, Deno, Bun, and V8 isolates.
Algorithm coverage is the best of any library here: fixed window, sliding window (weighted rolling average, eliminates boundary bursts), and token bucket. The peer dependency is @upstash/redis@^1.34.3 — you cannot point it at self-hosted Redis or Redis Cloud.
Upstash’s free tier includes 500,000 commands/month, which covers development and small production apps. The Vercel + Upstash integration is one-click from the Vercel marketplace. If your serverless app also needs durable background jobs, Upstash’s QStash is covered in Best job queue for Node.js and TypeScript.
// middleware.ts (Next.js 14+ / Vercel Edge)
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { NextRequest, NextResponse } from "next/server";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(), // UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN
limiter: Ratelimit.slidingWindow(100, "1 m"), // 100 req/min, sliding
analytics: true,
prefix: "app:ratelimit",
});
export const config = { matcher: "/api/:path*" };
export async function middleware(request: NextRequest) {
const forwarded = request.headers.get("x-forwarded-for");
const ip = forwarded ? forwarded.split(",")[0].trim() : request.ip ?? "anonymous";
const { success, limit, remaining, reset } = await ratelimit.limit(ip);
if (!success) {
return new NextResponse(
JSON.stringify({ error: "Rate limit exceeded" }),
{
status: 429,
headers: {
"Content-Type": "application/json",
"X-RateLimit-Limit": String(limit),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": new Date(reset).toISOString(),
},
}
);
}
const response = NextResponse.next();
response.headers.set("X-RateLimit-Limit", String(limit));
response.headers.set("X-RateLimit-Remaining", String(remaining));
return response;
}
Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in your Vercel environment variables — Upstash injects them automatically if you connect the integration.
DIY ioredis + Lua scripts
When you need an algorithm none of the above libraries implement, or when adding a dependency is constrained by compliance requirements. You own the Lua scripts; you own the bugs.
Redis’s official rate-limiting tutorial provides production-ready scripts for all five algorithms: fixed window, sliding window log, sliding window counter, token bucket, and leaky bucket. The sliding window counter is their recommended default — low memory, near-exact accuracy, cluster-safe via hash tags.
bottleneck — not recommended
bottleneck v2.19.5 was released in 2019. Last commit: 2020. A GitHub issue from December 2022 asking about maintenance received no authoritative reply. It is primarily an outbound rate limiter (throttling requests your code makes to external services), not an inbound API rate limiter. Its ~3–10M weekly downloads reflect legacy usage. Do not start new projects on it.
Comparison
| Library | Version | Weekly DL | Algorithm | TS types | Cluster-safe | Framework | Redis client |
|---|---|---|---|---|---|---|---|
rate-limiter-flexible | 11.1.0 | ~1.2–2M | Fixed window + burst | Built-in | Yes (Lua) | Agnostic | ioredis v5, node-redis v5 |
express-rate-limit + rate-limit-redis | 8.5.2 + 5.0.0 | ~24–38M (ERL total) | Fixed window | Built-in | Yes | Express only | node-redis, ioredis, others |
@upstash/ratelimit | 2.0.8 | ~357K | Fixed, sliding, token bucket | Built-in | Yes (HTTP) | Agnostic | @upstash/redis only |
| DIY ioredis + Lua | — | — | Any | N/A | Yes | Agnostic | ioredis v5 |
bottleneck | 2.19.5 | ~3–10M | Token bucket (outbound) | Built-in | Yes | Agnostic | ioredis (datastore) |
Picks by use case
Serverless / edge (Vercel, Cloudflare Workers, Lambda) → @upstash/ratelimit v2.0.8. It is the only connectionless option. Every other library needs a persistent TCP socket, which serverless runtimes do not support reliably. Start with Upstash’s free tier — 500,000 commands/month before you pay anything.
Monolith or microservices REST API → rate-limiter-flexible v11.1.0. Framework-agnostic, cluster-safe, actively maintained, and it includes a blockDuration for true DoS block strategies (not just rate limiting). The 0.7ms cluster-mode latency is within budget for almost any API.
Express app already using express-rate-limit → add rate-limit-redis v5.0.0. Two installs, five lines of code, no API changes. The fixed window limitation is acceptable for standard API protection; add a stricter authLimiter with a longer window for login endpoints.
Verdict
rate-limiter-flexible v11.1.0 is the default for any Node.js API with a real Redis instance. Framework-agnostic, actively maintained, cluster-safe, supports multiple Redis clients, and has block strategies beyond basic rate counting. For serverless and edge deployments, @upstash/ratelimit is not a tradeoff — it is the only option that works.
Caveats
@upstash/ratelimitv2.0.8 has not had a release in ~5 months as of June 2026. Core functionality is stable, but watch the GitHub repo for maintenance signals.- The
rate-limiter-flexiblebenchmark figures (0.7ms, 2.5ms) come from the library’s own documentation, not a third-party test. They are useful as a ballpark but not a guarantee against your topology. - The express-rate-limit download count is inflated — the majority of installs use the in-memory store. Do not compare it to
rate-limiter-flexible’s downloads as if they represent equivalent use cases. - This article contains affiliate links to Upstash. Upstash is genuinely the correct pick for serverless rate limiting and we would recommend it without the affiliate relationship. If that changes, the recommendation will change first.
References
| Source | Link |
|---|---|
| rate-limiter-flexible — GitHub | github.com/animir/node-rate-limiter-flexible |
| rate-limiter-flexible — Redis wiki | wiki/Redis |
| rate-limiter-flexible — releases | releases |
| rate-limiter-flexible — npm | npmjs.com/package/rate-limiter-flexible |
| express-rate-limit — npm | npmjs.com/package/express-rate-limit |
| rate-limit-redis — README | github.com/express-rate-limit/rate-limit-redis |
| @upstash/ratelimit — GitHub | github.com/upstash/ratelimit-js |
| @upstash/ratelimit — algorithms docs | upstash.com/docs/…/algorithms |
| @upstash/ratelimit — getting started | upstash.com/docs/…/gettingstarted |
| @upstash/ratelimit — npm | npmjs.com/package/@upstash/ratelimit |
| bottleneck — npm | npmjs.com/package/bottleneck |
| bottleneck — maintenance issue | github.com/SGrondin/bottleneck/issues/207 |
| Redis rate-limiting tutorial | redis.io/tutorials/howtos/ratelimiting |
| node-redis — cluster-slots race condition (issue #2704) | github.com/redis/node-redis/issues/2704 |
| node-redis — createCluster error-handling bug (issue #2721) | github.com/redis/node-redis/issues/2721 |