express-brute vs express-limiter vs express-rate-limit vs express-slow-down vs rate-limiter-flexible
Rate Limiting and Request Throttling in Express.js Applications
express-bruteexpress-limiterexpress-rate-limitexpress-slow-downrate-limiter-flexibleSimilar Packages:

Rate Limiting and Request Throttling in Express.js Applications

These five packages help protect Express.js applications from abuse by controlling how many requests clients can make. express-rate-limit is the most popular and actively maintained solution for basic rate limiting. express-slow-down works alongside it to gradually slow responses instead of blocking them. rate-limiter-flexible offers framework-agnostic flexibility with multiple storage backends. express-brute and express-limiter are older solutions with limited maintenance — they work but lack modern features and security updates.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
express-brute0568-219 years agoBSD
express-limiter0423-218 years agoMIT
express-rate-limit03,231141 kB84 months agoMIT
express-slow-down029938.2 kB44 months agoMIT
rate-limiter-flexible03,484198 kB1525 days agoISC

Rate Limiting in Express.js: Five Packages Compared

Protecting your Express.js applications from abuse requires smart rate limiting. These five packages (express-brute, express-limiter, express-rate-limit, express-slow-down, rate-limiter-flexible) all tackle this problem — but they work differently and suit different situations. Let's break down how they compare.

⚠️ Maintenance Status: Active vs Legacy

Before diving into features, know which packages are still maintained.

express-brute and express-limiter are legacy packages with minimal recent updates. They work, but don't expect new features or security patches.

express-rate-limit, express-slow-down, and rate-limiter-flexible are actively maintained with regular updates and better long-term support.

💡 Recommendation: For new projects, skip express-brute and express-limiter. Use express-rate-limit for simplicity or rate-limiter-flexible for advanced needs.

🔒 Basic Rate Limiting: Blocking Excessive Requests

All five packages can block clients who send too many requests. Here's how each handles it.

express-brute uses a brute-force protection approach with memory or Redis stores.

// express-brute: Basic setup
const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore();
const bruteforce = new ExpressBrute(store);

app.post('/login', bruteforce.prevent, (req, res) => {
  res.send('Login attempt processed');
});

express-limiter integrates with Redis for distributed rate limiting.

// express-limiter: Redis-backed limiting
const limiter = require('express-limiter')(app, redisClient);

app.use(limiter({
  path: '*',
  method: '*',
  limit: 100,
  expire: 60000
}));

express-rate-limit offers the cleanest API for standard rate limiting.

// express-rate-limit: Simple configuration
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests, please try again later'
});

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

express-slow-down doesn't block — it adds delay instead (see next section).

rate-limiter-flexible provides framework-agnostic rate limiting with multiple store options.

// rate-limiter-flexible: Flexible configuration
const { RateLimiterMemory } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterMemory({
  points: 10, // 10 requests
  duration: 1 // per 1 second
});

app.use(async (req, res, next) => {
  try {
    await rateLimiter.consume(req.ip);
    next();
  } catch (rej) {
    res.status(429).send('Too Many Requests');
  }
});

🐌 Slow Down vs Hard Block: Graceful Degradation

Sometimes you want to slow abusive clients instead of blocking them outright.

express-brute blocks after a threshold — no gradual slowdown.

// express-brute: Hard block after failures
bruteforce.handle((req, res, next, nextValidRequestDate) => {
  res.status(429).send('Too many requests');
});

express-limiter also blocks — no built-in slowdown feature.

// express-limiter: Returns 429 when limit exceeded
// No delay option available

express-rate-limit blocks by default, but you can customize the response.

// express-rate-limit: Custom block response
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  handler: (req, res) => {
    res.status(429).json({ error: 'Rate limit exceeded' });
  }
});

express-slow-down adds increasing delays before blocking occurs.

// express-slow-down: Gradual delay
const slowDown = require('express-slow-down');

const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50, // start delaying after 50 requests
  delayMs: 1000 // add 1 second delay per request
});

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

rate-limiter-flexible lets you implement custom slowdown logic manually.

// rate-limiter-flexible: Custom delay implementation
const rateLimiter = new RateLimiterMemory({
  points: 100,
  duration: 60,
  execEvenly: true // spreads requests evenly
});

// You can add custom delay logic based on remaining points

🗄️ Storage Backends: Memory, Redis, and Beyond

Where rate limit data is stored affects scalability and reliability.

express-brute supports MemoryStore and RedisStore.

// express-brute: Redis store
const RedisStore = require('express-brute-redis');
const store = new RedisStore({
  host: 'localhost',
  port: 6379
});

express-limiter requires Redis — no memory store option.

// express-limiter: Redis only
const limiter = require('express-limiter')(app, redisClient);
// Must provide Redis client

express-rate-limit works with memory (default) or Redis via external stores.

// express-rate-limit: Memory store (default)
const limiter = rateLimit({ windowMs: 60000, max: 100 });

// express-rate-limit: Redis store (via rate-limit-redis)
const RedisStore = require('rate-limit-redis');
const limiter = rateLimit({
  store: new RedisStore({ client: redisClient })
});

express-slow-down uses the same store options as express-rate-limit.

// express-slow-down: Redis store
const RedisStore = require('rate-limit-redis');
const slowDown = require('express-slow-down');

const speedLimiter = slowDown({
  store: new RedisStore({ client: redisClient })
});

rate-limiter-flexible supports the most stores: Memory, Redis, Memcached, MongoDB, PostgreSQL, and more.

// rate-limiter-flexible: Multiple store options
const { RateLimiterRedis } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 10,
  duration: 1
});

// Also supports: RateLimiterMemcache, RateLimiterMongo, RateLimiterPostgres

🎯 Key Identification: IP, User ID, or Custom Keys

How you identify clients changes based on your use case.

express-brute uses IP by default, with custom key support.

// express-brute: Custom key function
const bruteforce = new ExpressBrute(store, {
  getKey: (req, res, next) => next(req.body.username) // limit by username
});

express-limiter uses IP or custom lookup.

// express-limiter: Custom lookup
app.use(limiter({
  lookup: 'body.username' // limit by username in request body
}));

express-rate-limit uses IP by default, customizable via keyGenerator.

// express-rate-limit: Custom key generator
const limiter = rateLimit({
  keyGenerator: (req) => {
    return req.user?.id || req.ip; // limit by user ID if logged in
  }
});

express-slow-down uses the same keyGenerator option as express-rate-limit.

// express-slow-down: Custom key generator
const speedLimiter = slowDown({
  keyGenerator: (req) => {
    return req.user?.id || req.ip;
  }
});

rate-limiter-flexible gives you full control over key identification.

// rate-limiter-flexible: Manual key control
const key = req.user?.id || req.ip;
await rateLimiter.consume(key);
// You decide the key completely

📊 Feature Comparison Table

Featureexpress-bruteexpress-limiterexpress-rate-limitexpress-slow-downrate-limiter-flexible
Maintenance❌ Legacy❌ Legacy✅ Active✅ Active✅ Active
Default StoreMemoryRedisMemoryMemoryMemory
Redis Support✅ (external)✅ (external)
Other StoresLimitedLimited✅ Many
Slow Down⚠️ Manual
Hard Block⚠️ After delay
Custom Keys
Framework Agnostic
Ease of UseMediumMediumHighHighMedium

🌐 Real-World Scenarios

Scenario 1: Login Endpoint Protection

You need to prevent brute-force attacks on your login route.

  • Best choice: express-rate-limit
  • Why? Simple setup, blocks after threshold, well-documented.
const loginLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts per hour
  message: 'Too many login attempts, try again in an hour'
});

app.post('/login', loginLimiter, loginHandler);

Scenario 2: Public API with Graceful Degradation

You run a public API and want to discourage abuse without blocking legitimate users.

  • Best choice: express-slow-down + express-rate-limit
  • Why? Slow down first, block only if they keep pushing.
const slowDown = require('express-slow-down');
const rateLimit = require('express-rate-limit');

const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50,
  delayMs: 500
});

const hardLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});

app.use('/api/', speedLimiter, hardLimiter);

Scenario 3: Multi-Service Architecture

You have microservices using Express, Koa, and Fastify — need consistent rate limiting.

  • Best choice: rate-limiter-flexible
  • Why? Framework-agnostic, works across all services with same logic.
const { RateLimiterRedis } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 100,
  duration: 60
});

// Use same limiter in Express, Koa, Fastify services

Scenario 4: Legacy Application Maintenance

You're maintaining an old Express app already using express-brute.

  • Best choice: Keep express-brute (for now)
  • Why? Rewriting adds risk. Plan migration to express-rate-limit later.
// Keep existing express-brute setup
// Plan migration during next major refactor

Scenario 5: User-Based Rate Limiting

You need to limit requests per user account, not per IP.

  • Best choice: express-rate-limit or rate-limiter-flexible
  • Why? Both support custom key generators for user IDs.
// express-rate-limit approach
const userLimiter = rateLimit({
  keyGenerator: (req) => req.user?.id || req.ip
});

// rate-limiter-flexible approach
const key = req.user?.id || req.ip;
await rateLimiter.consume(key);

🚫 When Not to Use These

Consider alternatives when:

  • You need DDoS protection at the network level — use Cloudflare, AWS WAF, or nginx instead.
  • Your app runs on serverless/edge — check platform-specific rate limiting (Vercel, Cloudflare Workers).
  • You need complex business rules — build custom middleware with Redis directly.
  • Your traffic is very low — simple middleware might be overkill.

💡 Final Recommendation

Think about your project's needs:

  • Starting a new Express project? → Use express-rate-limit. It's simple, maintained, and covers 90% of use cases.
  • Need gradual slowdown? → Add express-slow-down alongside express-rate-limit.
  • Multiple frameworks or advanced stores? → Choose rate-limiter-flexible.
  • Maintaining legacy code? → Keep express-brute or express-limiter temporarily, but plan migration.

Final Thought: Rate limiting protects your application, but the right tool depends on your architecture. For most Express.js projects, express-rate-limit offers the best balance of simplicity and power. Save rate-limiter-flexible for complex scenarios, and avoid legacy packages in new code.

How to Choose: express-brute vs express-limiter vs express-rate-limit vs express-slow-down vs rate-limiter-flexible

  • express-brute:

    Choose express-brute only for legacy projects already using it. This package is no longer actively maintained and lacks modern security features. For new projects, prefer express-rate-limit or rate-limiter-flexible which receive regular updates and have better community support.

  • express-limiter:

    Choose express-limiter only if you're maintaining an existing codebase that depends on it. The package has minimal recent activity and fewer configuration options than modern alternatives. New projects should use express-rate-limit for better documentation and ongoing maintenance.

  • express-rate-limit:

    Choose express-rate-limit for most Express.js projects needing straightforward rate limiting. It has excellent documentation, active maintenance, and works well with Redis or memory stores. Ideal for APIs, login endpoints, and general request throttling where you need to block excessive requests cleanly.

  • express-slow-down:

    Choose express-slow-down when you want to degrade service gradually instead of hard blocking. Works best paired with express-rate-limit — slow down first, then block if clients keep pushing. Great for public APIs where you want to discourage abuse without immediately rejecting legitimate users who hit limits accidentally.

  • rate-limiter-flexible:

    Choose rate-limiter-flexible when you need framework-agnostic rate limiting or advanced features like multiple stores, custom keys, or complex rules. Works with Express, Koa, Fastify, and more. Best for microservices, multi-framework projects, or when you need fine-grained control over rate limiting logic.

README for express-brute

express-brute

NPM Version NPM Downloads Build Status Coverage Status Dependency Status

A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.

Installation

via npm:

  $ npm install express-brute

A Simple Example

var ExpressBrute = require('express-brute');

var store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
var bruteforce = new ExpressBrute(store);

app.post('/auth',
	bruteforce.prevent, // error 429 if we hit this route too often
	function (req, res, next) {
		res.send('Success!');
	}
);

Classes

ExpressBrute(store, options)

  • store An instance of ExpressBrute.MemoryStore or some other ExpressBrute store (see a list of known stores below).
  • options
    • freeRetries The number of retires the user has before they need to start waiting (default: 2)
    • minWait The initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds)
    • maxWait The maximum amount of time (in milliseconds) between requests the user needs to wait (default: 15 minutes). The wait for a given request is determined by adding the time the user needed to wait for the previous two requests.
    • lifetime The length of time (in seconds since the last request) to remember the number of requests that have been made by an IP. By default it will be set to maxWait * the number of attempts before you hit maxWait to discourage simply waiting for the lifetime to expire before resuming an attack. With default values this is about 6 hours.
    • failCallback Gets called with (req, resp, next, nextValidRequestDate) when a request is rejected (default: ExpressBrute.FailForbidden)
    • attachResetToRequest Specify whether or not a simplified reset method should be attached at req.brute.reset. The simplified method takes only a callback, and resets all ExpressBrute middleware that was called on the current request. If multiple instances of ExpressBrute have middleware on the same request, only those with attachResetToRequest set to true will be reset (default: true)
    • refreshTimeoutOnRequest Defines whether the lifetime counts from the time of the last request that ExpressBrute didn't prevent for a given IP (true) or from of that IP's first request (false). Useful for allowing limits over fixed periods of time, for example: a limited number of requests per day. (Default: true). More info
    • handleStoreError Gets called whenever an error occurs with the persistent store from which ExpressBrute cannot recover. It is passed an object containing the properties message (a description of the message), parent (the error raised by the session store), and [key, ip] or [req, res, next] depending on whether or the error occurs during reset or in the middleware itself.

ExpressBrute.MemoryStore()

An in-memory store for persisting request counts. Don't use this in production, instead choose one of the more robust store implementations listed below.

ExpressBrute Instance Methods

  • prevent(req, res, next) Middleware that will bounce requests that happen faster than the current wait time by calling failCallback. Equivilent to getMiddleware(null)
  • getMiddleware(options) Generates middleware that will bounce requests with the same key and IP address that happen faster than the current wait time by calling failCallback. Also attaches a function at req.brute.reset that can be called to reset the counter for the current ip and key. This functions as the reset instance method, but without the need to explicitly pass the ip and key paramters
    • key can be a string or alternatively it can be a function(req, res, next) that or calls next, passing a string as the first parameter.
    • failCallback Allows you to override the value of failCallback for this middleware
    • ignoreIP Disregard IP address when matching requests if set to true. Defaults to false.
  • reset(ip, key, next) Resets the wait time between requests back to its initial value. You can pass null for key if you want to reset a request protected by protect.

Built-in Failure Callbacks

There are some built-in callbacks that come with BruteExpress that handle some common use cases.

  • ExpressBrute.FailTooManyRquests Terminates the request and responses with a 429 (Too Many Requests) error that has a Retry-After header and a JSON error message.
  • ExpressBrute.FailForbidden Terminates the request and responds with a 403 (Forbidden) error that has a Retry-After header and a JSON error message. This is provided for compatibility with ExpressBrute versions prior to v0.5.0, for new users FailTooManyRequests is the preferred behavior.
  • ExpressBrute.FailMark Sets res.nextValidRequestDate, the Retry-After header and the res.status=429, then calls next() to pass the request on to the appropriate routes.

ExpressBrute stores

There are a number adapters that have been written to allow ExpressBrute to be used with different persistent storage implementations, some of the ones I know about include:

If you write your own store and want me to add it to the list, just drop me an email or create an issue.

A More Complex Example

require('connect-flash');
var ExpressBrute = require('express-brute'),
	MemcachedStore = require('express-brute-memcached'),
	moment = require('moment'),
    store;

if (config.environment == 'development'){
	store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
} else {
	// stores state with memcached
	store = new MemcachedStore(['127.0.0.1'], {
		prefix: 'NoConflicts'
	});
}

var failCallback = function (req, res, next, nextValidRequestDate) {
	req.flash('error', "You've made too many failed attempts in a short period of time, please try again "+moment(nextValidRequestDate).fromNow());
	res.redirect('/login'); // brute force protection triggered, send them back to the login page
};
var handleStoreError = handleStoreError: function (error) {
	log.error(error); // log this error so we can figure out what went wrong
	// cause node to exit, hopefully restarting the process fixes the problem
	throw {
		message: error.message,
		parent: error.parent
	};
}
// Start slowing requests after 5 failed attempts to do something for the same user
var userBruteforce = new ExpressBrute(store, {
	freeRetries: 5,
	minWait: 5*60*1000, // 5 minutes
	maxWait: 60*60*1000, // 1 hour,
	failCallback: failCallback,
	handleStoreError: handleStoreError
}
});
// No more than 1000 login attempts per day per IP
var globalBruteforce = new ExpressBrute(store, {
	freeRetries: 1000,
	attachResetToRequest: false,
	refreshTimeoutOnRequest: false,
	minWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
	maxWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
	lifetime: 24*60*60, // 1 day (seconds not milliseconds)
	failCallback: failCallback,
	handleStoreError: handleStoreError
});

app.set('trust proxy', 1); // Don't set to "true", it's not secure. Make sure it matches your environment
app.post('/auth',
	globalBruteforce.prevent,
	userBruteforce.getMiddleware({
		key: function(req, res, next) {
			// prevent too many attempts for the same username
			next(req.body.username);
		}
	}),
	function (req, res, next) {
		if (User.isValidLogin(req.body.username, req.body.password)) { // omitted for the sake of conciseness
		 	// reset the failure counter so next time they log in they get 5 tries again before the delays kick in
			req.brute.reset(function () {
				res.redirect('/'); // logged in, send them to the home page
			});
		} else {
			res.flash('error', "Invalid username or password")
			res.redirect('/login'); // bad username/password, send them back to the login page
		}
	}
);

Changelog

v1.0.1

  • BUG: Fixed an edge case where freeretries weren't being respected if app servers had slightly different times

v1.0.0

  • NEW: Updated to use Express 4.x as a peer dependency.
  • REMOVED: proxyDepth option on ExpressBrute has been removed. Use app.set('trust proxy', x) from Express 4 instead. More Info
  • REMOVED: getIPFromRequest(req) has been removed from instances, use req.ip instead.

v0.6.0

  • NEW: Added new ignoreIP option. (Thanks Magnitus-!)
  • CHANGED: .reset callbacks are now always called asyncronously, regardless of the implementation of the store (particularly effects MemoryStore).
  • CHANGED: Unit tests have been converted from Jasmine to Mocha/Chai/Sinon
  • BUG: Fixed a crash when .reset was called without a callback function

v0.5.3

  • NEW: Added the handleStoreError option to allow more customizable handling of errors that are thrown by the persistent store. Default behavior is to throw the errors as an exception - there is nothing ExpressBrute can do to recover.
  • CHANGED: Errors thrown as a result of errors raised by the store now include the store's error as well, for debugging purposes.

v0.5.2

  • CHANGED: Stopped using res.send(status, body), as it is deprecated in express 4.x. Instead call res.status and res.send separately (Thanks marinewater!)

v0.5.1

  • BUG: When setting proxyDepth to 1, ips is never populated with proxied X-Forwarded-For IP.

v0.5.0

  • NEW: Added an additional FailTooManyRequests failure callback, that returns a 429 (TooManyRequests) error instead of 403 (Forbidden). This is a more accurate error status code.
  • NEW: All the built in failure callbacks now set the "Retry-After" header to the number of seconds until it is safe to try again. Per RFC6585
  • NEW: Documentation updated to list some known store implementations.
  • CHANGED: Default failure callback is now FailTooManyRequests. FailForbidden remains an option for backwards compatiblity.
  • CHANGED: ExpressBrute.MemcachedStore is no longer included by default, and is now available as a separate module (because there are multiple store options it doesn't really make sense to include one by default).
  • CHANGED: FailMark no longer sets returns 403 Forbidden, instead does 429 TooManyRequets.

v0.4.2

  • BUG: In some cases when no callbacks were supplied memcached would drop the request. Ensure that memcached always sees a callback even if ExpressBrute isn't given one.

v0.4.1

  • NEW: refreshTimeoutOnRequest option that allows you to prevent the remaining lifetime for a timer from being reset on each request (useful for implementing limits for set time frames, e.g. requests per day)
  • BUG: Lifetimes were not previously getting extended properly for instances of ExpressBrute.MemoryStore

v0.4.0

  • NEW: attachResetToRequest parameter that lets you prevent the request object being decorated
  • NEW: failCallback can be overriden by getMiddleware
  • NEW: proxyDepth option on ExpressBrute that specifies how many levels of the X-Forwarded-For header to trust (inspired by express-bouncer).
  • NEW: getIPFromRequest method that essentially allows reset to used in a similar ways as in v0.2.2. This also respects the new proxyDepth setting.
  • CHANGED: getMiddleware now takes an options object instead of the key directly.

v0.3.0

  • NEW: Support for using custom keys to group requests further (e.g. grouping login requests by username)
  • NEW: Support for middleware from multiple instances of ExpressBrute on the same route.
  • NEW: Tracking lifetime now has a reasonable default derived from the other settings for that instance of ExpressBrute
  • NEW: Keys are now hashed before saving to a store, to prevent really long key names and reduce the possibility of collisions.
  • NEW: There is now a convience method that gets attached to req object as req.brute.reset. It takes a single parameter (a callback), and will reset all the counters used by ExpressBrute middleware that was called for the current route.
  • CHANGED: Tracking lifetime is now specified on ExpressBrute instead of MemcachedStore. This also means lifetime is now supported by MemoryStore.
  • CHANGED: The function signature for ExpressBrute.reset has changed. It now requires an IP and key be passed instead of a request object.
  • IMPROVED: Efficiency for large values of freeRetries.
  • BUG: Removed a small chance of incorrectly triggering brute force protection.