rate-limiter-flexible vs express-slow-down vs express-brute vs express-rate-limit
Protecting APIs from Abuse: Rate Limiting Strategies in Express
rate-limiter-flexibleexpress-slow-downexpress-bruteexpress-rate-limitSimilar 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
rate-limiter-flexible2,361,3793,546227 kB1114 days agoISC
express-slow-down146,58330137.6 kB12 months agoMIT
express-brute0569-2110 years agoBSD
express-rate-limit03,257146 kB105 days agoMIT

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: rate-limiter-flexible vs express-slow-down vs express-brute vs express-rate-limit

  • 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.

  • 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.

  • 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.

README for rate-limiter-flexible

npm version npm node version deno version

Logo

node-rate-limiter-flexible

rate-limiter-flexible counts and limits the number of events and protects from DoS and brute force attacks at any scale.

It works with Valkey, Redis, Prisma, DynamoDB, process Memory, Cluster or PM2, Memcached, MongoDB, MySQL, SQLite, and PostgreSQL.

Memory limiter also works in the browser.

AI tools See llms.txt and CONTEXT.md for LLM-friendly documentation.

Atomic increments. All operations in memory or distributed environment use atomic increments against race conditions.

Fast. Average request takes 0.7ms in Cluster and 2.5ms in Distributed application. See benchmarks.

Flexible. Combine limiters, block key for some duration, delay actions, manage failover with insurance options, configure smart key blocking in memory and many others.

Ready for growth. It provides a unified API for all limiters. Whenever your application grows, it is ready. Prepare your limiters in minutes.

Friendly. No matter which node package you prefer: valkey-glide or iovalkey, redis or ioredis, sequelize/typeorm or knex, memcached, native driver or mongoose. It works with all of them.

In-memory blocks. Avoid extra requests to store with inMemoryBlockOnConsumed.

Deno compatible See this example

The Flexible Fixed Window algorithm starts counting from the moment a request is received, diversifying rate limit reset times across clients. Read more here

Installation

npm i --save rate-limiter-flexible

yarn add rate-limiter-flexible

Import

import { RateLimiterMemory } from "rate-limiter-flexible";

// or import directly
import RateLimiterMemory from "rate-limiter-flexible/lib/RateLimiterMemory.js";

Basic Example

Points can be consumed by IP address, user ID, authorisation token, API route or any other string.

const opts = {
  points: 6, // 6 points
  duration: 1, // Per second
};

const rateLimiter = new RateLimiterMemory(opts);

rateLimiter.consume(remoteAddress, 2) // consume 2 points
    .then((rateLimiterRes) => {
      // 2 points consumed
    })
    .catch((rateLimiterRes) => {
      // Not enough points to consume
    });

RateLimiterRes object

The Promise's resolve and reject callbacks both return an instance of the RateLimiterRes class if there is no error. Object attributes:

RateLimiterRes = {
    msBeforeNext: 250, // Number of milliseconds before next action can be done
    remainingPoints: 0, // Number of remaining points in current duration 
    consumedPoints: 5, // Number of consumed points in current duration 
    isFirstInDuration: false, // action is first in current duration 
}

You may want to set HTTP headers for the response:

const headers = {
  "Retry-After": rateLimiterRes.msBeforeNext / 1000,
  "X-RateLimit-Limit": opts.points,
  "X-RateLimit-Remaining": rateLimiterRes.remainingPoints,
  "X-RateLimit-Reset": Math.ceil((Date.now() + rateLimiterRes.msBeforeNext) / 1000)
}

Advantages:

Full documentation is on Wiki

Middlewares, plugins and other packages

Copy/paste examples on Wiki:

Migration from other packages

  • express-brute Bonus: race conditions fixed, prod deps removed
  • limiter Bonus: multi-server support, respects queue order, native promises

Docs and Examples

Changelog

See releases for detailed changelog.

Basic Options

  • points

    Required

    Maximum number of points that can be consumed over duration

  • duration

    Required

    Number of seconds before consumed points are reset.

    Points are never reset if duration is set to 0.

  • storeClient

    Required for store limiters

    Must be pool or connection created with store client packages, e.g. @valkey/valkey-glide, ioredis, iovalkey, redis, memcached, mongodb, pg, mysql2, mysql, etc.

Other options on Wiki:

See full list of options.

API

Read detailed description on Wiki.

Contributions

Appreciated, feel free!

Make sure you've launched npm run eslint before creating PR, all errors have to be fixed.

You can try to run npm run eslint-fix to fix some issues.

Any new limiter with storage must be extended from RateLimiterStoreAbstract. It has to implement 4 methods:

  • _getRateLimiterRes parses raw data from store to RateLimiterRes object.

  • _upsert may be atomic or non-atomic upsert (increment). It inserts or updates the value by key and returns raw data. If it doesn't make an atomic upsert (increment), the class should be suffixed with NonAtomic, e.g. RateLimiterRedisNonAtomic.

    It must support forceExpire mode to overwrite key expiration time.

  • _get returns raw data by key or null if there is no key.

  • _delete deletes all key-related data and returns true on deleted, false if key is not found.

All other methods depend on the store. See RateLimiterRedis or RateLimiterPostgres for examples.

For wrapper classes that don't need full RateLimiterAbstract functionality, extend RateLimiterCompatibleAbstract instead. It requires implementing consume, penalty, reward, get, set, block, delete methods and blockDuration/execEvenly getters/setters. If the wrapper doesn't use blockDuration or execEvenly, empty no-op implementations can be provided. See RLWrapperBlackAndWhite for an example.

Note: all changes should be covered by tests.