Skip to main content

Best Express Rate Limiting Packages 2026

·PkgPulse Team
0

TL;DR

express-rate-limit for the standard Express API rate limiting use case. rate-limiter-flexible when you need Redis-backed distributed rate limiting, advanced sliding windows, or multi-tier limits. Bottleneck for outbound request throttling. Most Express applications need only express-rate-limit — it covers IP-based limits in 10 lines of code with no infrastructure dependencies. If you're running multiple Express servers behind a load balancer and need limits to be shared across instances, rate-limiter-flexible with Redis is the answer. Bottleneck is a different category: it throttles your app's outbound calls to external APIs, not inbound requests.

Quick Comparison

express-rate-limit v7rate-limiter-flexible v5Bottleneck v2
Weekly Downloads~4M~1.8M~1.2M
GitHub Stars~11K~5K~2K
Bundle Size~15KB~40KB~22KB
Redis SupportVia external storeBuilt-inBuilt-in
Distributed (multi-server)Needs store adapterYesYes (queue)
Primary Use CaseInbound API limitsInbound + distributedOutbound throttling
Sliding WindowApproximateTrue sliding windowYes (reservoir)
Per-User LimitsYes (custom key)YesYes
TypeScriptYesYesYes
LicenseMITISCMIT

express-rate-limit: The Standard Choice

express-rate-limit is the default npm package for Express rate limiting. It's used in over 150,000 public repositories and covers the most common use case — IP-based request throttling — in minimal code:

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  limit: 100,               // max 100 requests per window
  standardHeaders: 'draft-8', // Return RateLimit headers per RFC 6585
  legacyHeaders: false,
  // Optional: custom message
  message: { error: 'Too many requests, please try again later.' },
});

app.use('/api', limiter);

For most single-server applications, this is all you need. The response headers tell clients exactly when they can retry:

RateLimit-Limit: 100
RateLimit-Remaining: 47
RateLimit-Reset: 2026-04-13T15:30:00.000Z
Retry-After: 847

Custom Keys: Per-User and Per-Route Limits

The keyGenerator option lets you rate-limit by anything — user ID, API key, or a combination:

const apiKeyLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  limit: 60,
  keyGenerator: (req) => {
    // Limit by API key when present, fall back to IP
    return req.headers['x-api-key'] || req.ip;
  },
  skip: (req) => {
    // Don't rate limit internal health checks
    return req.path === '/health';
  },
});

// More aggressive limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 10, // only 10 login attempts per 15 min
  keyGenerator: (req) => req.ip,
});

app.post('/auth/login', authLimiter, loginHandler);
app.post('/auth/register', authLimiter, registerHandler);
app.use('/api', apiKeyLimiter);

The Distributed Limitation

express-rate-limit's default memory store is in-process — each running instance has its own counter. If you deploy three Express servers behind a load balancer, each tracks limits independently, effectively tripling your rate limit from the user's perspective.

For single-server deployments, this is fine. For distributed setups, you need a shared store:

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 100,
  store: new RedisStore({
    sendCommand: (...args) => client.sendCommand(args),
  }),
});

The rate-limit-redis adapter works well, but at this point you're adding Redis infrastructure anyway — which is when rate-limiter-flexible becomes worth evaluating.


rate-limiter-flexible: Distributed and Production-Grade

rate-limiter-flexible is designed for production multi-server setups from the start. It supports Redis, Memcached, MongoDB, and Postgres as backing stores, and its rate limiting algorithms are more sophisticated than express-rate-limit's fixed window:

import { RateLimiterRedis } from 'rate-limiter-flexible';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });

const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'api',
  points: 100,       // 100 requests
  duration: 900,     // per 15 minutes
  blockDuration: 60, // block for 1 minute if exceeded
});

// Express middleware:
const rateLimiterMiddleware = async (req, res, next) => {
  try {
    await rateLimiter.consume(req.ip);
    next();
  } catch (rejRes) {
    const secs = Math.round(rejRes.msBeforeNextReset / 1000) || 1;
    res.set('Retry-After', String(secs));
    res.status(429).json({ error: 'Too many requests' });
  }
};

app.use('/api', rateLimiterMiddleware);

True Sliding Window Algorithm

express-rate-limit uses a fixed window by default: if the window is 15 minutes and you send 100 requests at 14:59, the limit resets at 15:00 and you can send 100 more immediately — allowing a burst of 200 requests in ~1 minute at window boundaries.

rate-limiter-flexible's sliding window algorithm prevents this burst pattern:

import { RateLimiterRedis } from 'rate-limiter-flexible';

// Sliding window: no burst at window boundaries
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 100,
  duration: 900,
  // No explicit window type needed — sliding window is the default
  // Uses token bucket / leaky bucket internals
});

With a true sliding window, the 100-request limit applies across any rolling 15-minute period, not just calendar windows. This prevents the burst exploitation that's possible with fixed window rate limiters.

Multi-Tier Rate Limiting

rate-limiter-flexible supports stacking multiple limiters for tiered enforcement — for example, a per-second burst limit combined with a per-day sustained limit:

import { RateLimiterRedis, RateLimiterUnion } from 'rate-limiter-flexible';

const perSecond = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'per_second',
  points: 5,      // 5 requests per second (burst)
  duration: 1,
});

const perDay = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'per_day',
  points: 10000,  // 10,000 per day (sustained)
  duration: 86400,
});

const combined = new RateLimiterUnion(perSecond, perDay);

app.use('/api', async (req, res, next) => {
  try {
    await combined.consume(req.ip);
    next();
  } catch {
    res.status(429).json({ error: 'Rate limit exceeded' });
  }
});

This pattern is common in public APIs that need both burst protection and daily usage caps. Express-rate-limit requires two separate middleware instances for this; rate-limiter-flexible's RateLimiterUnion handles it atomically.


Bottleneck: Outbound Rate Limiting

Bottleneck solves a different problem: controlling how fast your application sends requests to external services. If you're calling a third-party API that enforces rate limits, Bottleneck prevents you from exceeding their limits rather than limiting who can call you.

import Bottleneck from 'bottleneck';

// GitHub API allows 5,000 requests/hour for authenticated users
const limiter = new Bottleneck({
  maxConcurrent: 10,         // max 10 in-flight requests at once
  minTime: 720,              // minimum 720ms between requests (~83/minute)
  reservoir: 5000,           // initial bucket of 5000 requests
  reservoirRefreshAmount: 5000,
  reservoirRefreshInterval: 60 * 60 * 1000, // refill hourly
});

// Wrap any async function:
const fetchGitHubRepo = limiter.wrap(async (owner, repo) => {
  const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
  return response.json();
});

// These calls are automatically throttled:
const results = await Promise.all([
  fetchGitHubRepo('facebook', 'react'),
  fetchGitHubRepo('vercel', 'next.js'),
  fetchGitHubRepo('tailwindlabs', 'tailwindcss'),
  // ... 100 more — Bottleneck queues them automatically
]);

Bottleneck also supports Redis for distributed outbound throttling — useful when you have multiple servers all calling the same external API:

const limiter = new Bottleneck({
  maxConcurrent: 5,
  minTime: 200,
  id: 'github-api',            // shared queue ID
  datastore: 'ioredis',        // distributed queue via Redis
  clearDatastore: false,
  clientOptions: {
    host: process.env.REDIS_HOST,
    port: 6379,
  },
});

If you're scraping data, calling LLM APIs, or hitting payment processors from multiple workers, Bottleneck is the right tool. It's not a replacement for inbound rate limiting — it's a complement to it.


When to Use Which

Choose express-rate-limit when:

  • Single-server Express API with standard IP-based rate limits
  • Quick setup with minimal configuration
  • You don't need Redis yet (or you can add the Redis store adapter later)
  • Protecting specific routes from brute force (auth endpoints, password reset)

Choose rate-limiter-flexible when:

  • Multi-server or load-balanced deployment requiring shared state
  • You need true sliding window algorithms (no burst exploitation at window boundaries)
  • Multi-tier limits (burst + sustained) on the same endpoint
  • You're already using Redis and want a single package that handles all store types
  • Building a public API with strict SLA guarantees on rate limits

Choose Bottleneck when:

  • Throttling your app's outbound requests to external APIs
  • Controlling concurrency and request queuing
  • Respecting third-party API rate limits from multiple workers
  • Any use case involving "how fast can we call this service"

Practical Setup: express-rate-limit + Redis for Production

The most common production setup combines express-rate-limit's familiar API with the Redis store for distributed enforcement:

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 globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 500,
  standardHeaders: 'draft-8',
  legacyHeaders: false,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
});

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 10,
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
    prefix: 'auth',
  }),
});

app.use('/api', globalLimiter);
app.post('/auth/login', authLimiter);
app.post('/auth/register', authLimiter);

This pattern scales to most production Express applications without requiring rate-limiter-flexible's additional complexity. Graduate to rate-limiter-flexible if you need sliding windows or multi-tier enforcement.

See also: express-rate-limit on PkgPulse, helmet vs cors vs express-rate-limit: Express Security Packages 2026, and Express vs Fastify 2026.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.