express-brute vs express-rate-limit vs express-slow-down vs rate-limiter-flexible
Protecting APIs from Abuse: Rate Limiting Strategies in Express
express-bruteexpress-rate-limitexpress-slow-downrate-limiter-flexibleSimilar Packages:

Protecting APIs from Abuse: Rate Limiting Strategies in Express

These libraries help secure Node.js servers by controlling how many requests a client can make in a given time. They prevent brute-force attacks, DDoS attempts, and API abuse. While express-rate-limit is the standard for simple blocking, express-slow-down adds delay tactics, and rate-limiter-flexible offers deep customization across different frameworks. express-brute is an older solution that is no longer maintained.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
express-brute0568-2110 years agoBSD
express-rate-limit03,259146 kB924 days agoMIT
express-slow-down030037.6 kB13 months agoMIT
rate-limiter-flexible03,553227 kB132 days agoISC

Protecting APIs from Abuse: Rate Limiting Strategies in Express

Securing a backend service often starts with controlling traffic. The packages express-brute, express-rate-limit, express-slow-down, and rate-limiter-flexible all aim to stop abuse, but they use different methods and have different levels of support. Let's look at how they handle real-world engineering challenges.

🛠️ Maintenance and Project Health

Project health matters because security libraries need updates. If a package is abandoned, it becomes a risk.

express-brute is no longer maintained.

  • The repository has not seen significant updates in years.
  • Using it introduces potential security vulnerabilities without fixes.
// express-brute: Legacy setup (Not recommended)
const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore();
const bruteforce = new ExpressBrute(store);
// ⚠️ Warning: No longer receiving security patches

express-rate-limit is actively maintained.

  • It receives regular updates for security and performance.
  • Widely adopted in the Express ecosystem.
// express-rate-limit: Active setup
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});
// ✅ Regularly updated and secure

express-slow-down is actively maintained.

  • Often updated alongside express-rate-limit.
  • Focused specifically on delay tactics.
// express-slow-down: Active setup
const slowDown = require('express-slow-down');
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 100,
  delayMs: 500
});
// ✅ Regularly updated and secure

rate-limiter-flexible is actively maintained.

  • Supports many modern stores and frameworks.
  • Active community and documentation.
// rate-limiter-flexible: Active setup
const { RateLimiterRedis } = require('rate-limiter-flexible');
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 10,
  duration: 1
});
// ✅ Regularly updated and secure

⚙️ Basic Configuration and Setup

Setting up limits should be clear. Each package handles configuration differently.

express-brute uses a constructor with store options.

  • You define the store first, then the middleware.
  • Configuration is tied to the instance creation.
// express-brute: Constructor based
const failCallback = (req, res, next, nextValidRequestDate) => {
  res.status(429).send('Too many requests');
};
const bruteforce = new ExpressBrute(store, {
  failCallback: failCallback,
  minWait: 500,
  maxWait: 1000 * 60 * 60
});
app.post('/login', bruteforce.prevent, loginHandler);

express-rate-limit uses a simple options object.

  • Configuration is passed directly to the main function.
  • Very readable for standard limits.
// express-rate-limit: Options object
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: 'Too many requests from this IP'
});
app.use('/api/', limiter);

express-slow-down uses delay-specific options.

  • You configure when delays start and how long they last.
  • Focuses on time penalties rather than hard blocks.
// express-slow-down: Delay options
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50,
  delayMs: (hits) => hits * 1000
});
app.use('/api/', speedLimiter);

rate-limiter-flexible uses class instantiation.

  • You create a limiter object with strict typing options.
  • Requires more setup but allows complex rules.
// rate-limiter-flexible: Class instantiation
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  keyPrefix: 'middleware',
  points: 10,
  duration: 1,
  blockDuration: 60
});
app.use('/api/', (req, res, next) => {
  rateLimiter.consume(req.ip)
    .then(() => next())
    .catch(() => res.status(429).send('Too Many Requests'));
});

💾 Storage Backends and Scalability

Where you store request counts affects performance and scaling.

express-brute supports Memory and Redis.

  • Memory store is for single-instance apps only.
  • Redis allows multiple servers to share limit data.
// express-brute: Redis Store
const RedisStore = require('express-brute-redis');
const store = new RedisStore({
  host: '127.0.0.1',
  port: 6379
});
// ⚠️ Limited store options compared to modern tools

express-rate-limit supports Memory, Redis, and Memcached.

  • Default is Memory, which resets on restart.
  • Redis store is recommended for production clusters.
// express-rate-limit: Redis Store
const RedisStore = require('rate-limit-redis');
const limiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
  }),
  windowMs: 15 * 60 * 1000,
  max: 100
});
// ✅ Standard store integrations

express-slow-down shares stores with express-rate-limit.

  • Uses the same underlying storage mechanisms.
  • Ensures consistency when used together.
// express-slow-down: Redis Store
const RedisStore = require('rate-limit-redis');
const speedLimiter = slowDown({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
  }),
  windowMs: 15 * 60 * 1000
});
// ✅ Consistent storage with rate-limit

rate-limiter-flexible supports many databases.

  • Works with Redis, Memcached, MongoDB, Postgres, and more.
  • Best choice if you already have a specific database infrastructure.
// rate-limiter-flexible: Mongo Store
const { RateLimiterMongo } = require('rate-limiter-flexible');
const rateLimiter = new RateLimiterMongo({
  storeClient: mongoClient,
  tableName: 'rate_limits',
  points: 10,
  duration: 1
});
// ✅ Wide variety of supported stores

🚦 Response Behavior: Block vs Delay

How the server reacts to too many requests changes the user experience.

express-brute blocks requests after a limit.

  • It returns an error immediately.
  • Can implement progressive wait times before blocking.
// express-brute: Progressive blocking
const bruteforce = new ExpressBrute(store, {
  minWait: 500,
  maxWait: 1000 * 60,
  failCallback: (req, res) => res.status(429).send('Blocked')
});
// ❌ Hard block after retries exhausted

express-rate-limit blocks requests after a limit.

  • Returns HTTP 429 Too Many Requests.
  • Simple and effective for hard limits.
// express-rate-limit: Hard block
const limiter = rateLimit({
  max: 5,
  handler: (req, res) => {
    res.status(429).send('Too many requests, please try again later.');
  }
});
// ❌ Hard block immediately after limit

express-slow-down delays responses instead of blocking.

  • Requests still succeed but take longer.
  • Discourages bots without breaking legitimate users.
// express-slow-down: Delay response
const speedLimiter = slowDown({
  delayAfter: 5,
  delayMs: (hits) => hits * 1000 // 1s, 2s, 3s...
});
// ✅ Slows down traffic instead of dropping it

rate-limiter-flexible allows custom actions.

  • You decide what happens in the .catch() block.
  • Can block, delay manually, or log and allow.
// rate-limiter-flexible: Custom action
rateLimiter.consume(key)
  .then(() => next())
  .catch((rej) => {
    if (rej.msBeforeNext) {
      res.setHeader('Retry-After', Math.ceil(rej.msBeforeNext / 1000));
    }
    res.status(429).send('Limit exceeded');
  });
// ✅ Fully customizable response logic

🌐 Framework Support and Flexibility

Some tools work only with Express, while others fit anywhere.

express-brute is Express only.

  • Middleware is tightly coupled to Express request objects.
  • Cannot be used with Koa, Fastify, or NestJS easily.
// express-brute: Express middleware
app.use('/auth', bruteforce.prevent);
// ❌ Tightly coupled to Express

express-rate-limit is Express only.

  • Designed specifically for the Express middleware stack.
  • Does not work outside the Express ecosystem.
// express-rate-limit: Express middleware
app.use(limiter);
// ❌ Tightly coupled to Express

express-slow-down is Express only.

  • Companion to express-rate-limit.
  • Shares the same ecosystem limitations.
// express-slow-down: Express middleware
app.use(speedLimiter);
// ❌ Tightly coupled to Express

rate-limiter-flexible works with many frameworks.

  • Supports Express, Koa, NestJS, Hapi, and raw Node.js.
  • Logic is separated from the framework layer.
// rate-limiter-flexible: Framework agnostic
// Works in Express
app.use((req, res, next) => { /* consume */ });
// Works in Koa
app.use(async (ctx, next) => { /* consume */ });
// ✅ Works across multiple frameworks

📊 Summary: Key Differences

Featureexpress-bruteexpress-rate-limitexpress-slow-downrate-limiter-flexible
Status⚠️ Deprecated✅ Active✅ Active✅ Active
Primary ActionBlockBlockDelayBlock / Custom
StorageMemory, RedisMemory, Redis, MemcachedMemory, Redis, MemcachedRedis, Mongo, Postgres, etc.
FrameworkExpressExpressExpressAny (Express, Koa, etc.)
ComplexityLowLowLowMedium

💡 The Big Picture

express-brute is a legacy tool that should be replaced. It served a purpose in the past but lacks modern support. Do not start new projects with it.

express-rate-limit is the go-to for standard Express apps. It is simple, reliable, and solves 90% of use cases with minimal code. Pair it with express-slow-down if you need to throttle traffic before blocking it.

express-slow-down is a specialized tool for DDoS mitigation. It makes attacks expensive for the attacker without cutting off users entirely. It works best as a companion to a hard rate limiter.

rate-limiter-flexible is the power user choice. If you need to share limits across different services, use non-Redis databases, or work outside of Express, this is the right pick. It requires more setup but scales better with complex architectures.

Final Thought: For most teams, start with express-rate-limit. It is the path of least resistance. Move to rate-limiter-flexible only when your architecture outgrows the Express ecosystem or requires specific database integrations.

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

  • express-brute:

    Avoid this package for new projects as it is deprecated and no longer maintained. It lacks security updates and modern features found in newer alternatives. Only consider it if you are maintaining a legacy system that already depends on it and cannot be refactored immediately.

  • express-rate-limit:

    Choose express-rate-limit for standard Express applications that need simple, reliable IP-based rate limiting. It is easy to set up with minimal configuration and works well with in-memory or Redis stores. This is the best starting point for most REST APIs and server-side rendered apps.

  • express-slow-down:

    Select express-slow-down when you want to throttle abusive clients by slowing their responses instead of blocking them outright. It works best when paired with express-rate-limit to create a layered defense strategy. This approach is useful for mitigating DDoS attacks without dropping connections immediately.

  • rate-limiter-flexible:

    Use rate-limiter-flexible if you need advanced features like multi-store support, custom key generation, or integration with frameworks other than Express. It provides fine-grained control over points consumption and is ideal for complex microservices architectures. The setup is more involved but offers greater long-term flexibility.

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.