bottleneck、express-rate-limit、limiter、p-limit、rate-limiter-flexible 和 ratelimiter 都是用于在 Node.js 环境中管理请求频率和并发任务的工具,但它们的侧重点截然不同。
p-limit 是一个极简的并发控制库,专注于限制同时执行的 Promise 数量,适合客户端或简单的脚本任务。bottleneck 则是一个功能强大的作业调度器,不仅限制并发,还能智能排队、优先处理任务并自动适应下游服务的速率限制,非常适合调用第三方 API。
在服务端防护方面,express-rate-limit 是 Express 框架的标准中间件,用于防止暴力破解和 DDoS 攻击。rate-limiter-flexible 提供了更高级的架构支持,内置多种存储后端(如 Redis),适合微服务和分布式系统。limiter 是一个老牌库,采用令牌桶算法,但在现代异步场景中略显笨重。而 ratelimiter 目前已被官方标记为弃用(deprecated),不应在新项目中使用。
在构建高可用的 Node.js 应用时,控制“速度”至关重要。这通常分为两个场景:一是** outbound(出站),即我们要控制自己调用第三方 API 的频率,避免被封禁;二是 inbound(入站)**,即我们要限制用户对我们的服务器发起请求的频率,防止服务过载。
本文对比的六个包涵盖了这两个场景的不同需求。让我们深入看看它们在实际工程中是如何工作的。
首先必须明确一个概念:并发控制(Concurrency Control) 和 速率限制(Rate Limiting) 是不同的。
p-limit 是典型的并发控制工具,而 express-rate-limit 和 rate-limiter-flexible 则是典型的速率限制工具。bottleneck 则是两者的集大成者。
当你需要调用外部 API(例如抓取数据、发送邮件)时,直接发起请求往往会导致被对方服务器拒绝(HTTP 429)。这时你需要一个能够“排队”并“平滑发送”请求的工具。
p-limit:最简单的并发锁如果你只需要限制同时运行的任务数量,而不关心时间窗口,p-limit 是最轻量的选择。
import pLimit from 'p-limit';
// 限制同时只有 2 个任务运行
const limit = pLimit(2);
const input = [1, 2, 3, 4, 5];
// 映射所有任务,但它们会排队执行
const result = await Promise.all(input.map(async (val) => {
return limit(() => {
console.log(`Starting task ${val}`);
return fetch(`https://api.example.com/data/${val}`);
});
}));
适用场景:前端图片懒加载、简单的脚本批量处理。它没有内置的延迟机制,只是单纯地暂停启动新任务。
bottleneck:智能作业调度器bottleneck 远不止是限制并发。它能模拟“令牌桶”或“固定窗口”,自动计算何时发送下一个请求,甚至支持优先级队列。
import Bottleneck from 'bottleneck';
// 配置:每 1000ms 最多运行 5 个任务,且任务之间至少间隔 200ms
const limiter = new Bottleneck({
minTime: 200,
maxConcurrent: 5,
highWater: 100, // 队列长度警告阈值
strategy: Bottleneck.strategy.OVERFLOW // 队列满时的策略
});
// 提交任务
limiter.schedule(() => fetch('https://api.github.com/users'), { priority: 1 })
.then(res => console.log('Done'));
// 监听队列状态
limiter.on('highwater', () => {
console.warn('队列积压过多,考虑降低发送速度');
});
适用场景:调用 Stripe、GitHub、Google Maps 等有严格速率限制的 API。它能保证你的请求曲线是平滑的,而不是突发式的。
limiter:老派的令牌桶limiter 是一个较老的库,它实现了经典的令牌桶算法。虽然功能可靠,但其 API 设计偏向回调风格,对 Promise 的支持不如前两者自然。
import { TokenBucket } from 'limiter';
// 每 1000ms 补充 1 个令牌,桶容量为 5
const bucket = new TokenBucket(1, 1000, null, 5);
bucket.removeTokens(1, (err, remainingTokens) => {
if (err) throw err;
// 拿到令牌后执行请求
fetch('https://api.example.com/data');
console.log(`剩余令牌: ${remainingTokens}`);
});
适用场景:维护旧项目,或者需要精确控制令牌补充速率且不喜欢 bottleneck 复杂配置的场景。在新项目中,通常推荐 bottleneck。
当面对用户请求时,你需要防止单个 IP 或用户刷爆你的接口。
express-rate-limit:Express 的标准卫士这是 Express 生态中最流行的中间件。它默认使用内存存储,配置简单,足以应对大多数单体应用。
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟窗口
max: 100, // 每个 IP 最多 100 次请求
message: '请求过于频繁,请稍后再试',
standardHeaders: true, // 返回 RateLimit-* 头
legacyHeaders: false,
});
app.use('/api/', limiter);
适用场景:标准的 Express 后端,单实例部署。如果你的应用部署在多个实例上且没有共享存储(如 Redis),每个实例会独立计数,可能导致限制不精确。
rate-limiter-flexible:分布式系统的利器当你的应用部署在多个容器或服务器(集群模式)时,内存存储就不再可靠了。rate-limiter-flexible 支持 Redis、Memcached 等多种后端,确保全局限流一致。
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';
const redisClient = new Redis();
const limiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'middleware',
points: 10, // 10 次请求
duration: 1, // 每 1 秒
});
// 在中间件中使用
app.use(async (req, res, next) => {
try {
await limiter.consume(req.ip);
next();
} catch (rejRes) {
res.status(429).send('Too Many Requests');
}
});
适用场景:微服务架构、高并发系统、需要精确全局限流的场景。它也支持非 Express 环境(如 Koa、NestJS 或纯 HTTP 模块)。
ratelimiter:⚠️ 已弃用,请勿使用ratelimiter 曾经是一个流行的通用限流库,但目前已正式弃用(Deprecated)。其维护者建议所有用户迁移到 rate-limiter-flexible。
// ❌ 不要在新项目中这样做
// const Limiter = require('ratelimiter');
// 该包不再接收安全更新,且缺乏现代存储适配
建议:如果你正在使用它,请制定计划迁移到 rate-limiter-flexible,以获得更好的性能和安全性。
| 特性 | p-limit | bottleneck | express-rate-limit | rate-limiter-flexible | limiter | ratelimiter |
|---|---|---|---|---|---|---|
| 主要用途 | 并发控制 (Outbound) | 智能调度 (Outbound) | 基础限流 (Inbound) | 分布式限流 (Inbound) | 令牌桶 (通用) | ❌ 已弃用 |
| 存储后端 | 内存 (无状态) | 内存 / Redis | 内存 / 自定义 | Redis, Memcached, DB | 内存 | 内存 / Redis |
| 集群支持 | ❌ | ✅ (需配置) | ❌ (默认) | ✅ 原生支持 | ❌ | ✅ |
| Promise 支持 | ✅ 原生 | ✅ 原生 (schedule) | ✅ (中间件) | ✅ (consume) | ⚠️ 回调为主 | ✅ |
| 优先级队列 | ❌ | ✅ 支持 | ❌ | ❌ | ❌ | ❌ |
| 框架依赖 | 无 | 无 | Express | 无 (框架无关) | 无 | 无 |
bottleneck。它的 minTime 和 maxConcurrent 组合能完美模拟大多数 API 的速率限制策略,且内置的队列管理能防止内存溢出。p-limit。它体积极小,没有多余依赖,解决“同时只能跑 5 个”这类问题绰绰有余。express-rate-limit。配置最简单,文档最丰富,能满足 90% 的防刷需求。rate-limiter-flexible 配合 Redis。只有共享存储才能保证在水平扩展时限流规则依然有效。limiter,理解其令牌桶逻辑即可;如果遇到 ratelimiter,请尽快替换。限流不仅仅是加个锁,它是系统稳定性的基石。选择正确的工具,能让你的应用在流量洪峰中依然稳如泰山。
选择 express-rate-limit 如果你正在构建标准的 Express 应用,并且只需要一个简单的中间件来限制每个 IP 或用户的请求频率以防止滥用。它是上手最快、社区最成熟的 Express 专用解决方案。
切勿在新项目中选择 ratelimiter。该包已在 npm 上被官方标记为弃用(deprecated),其功能已被更现代、维护更活跃的 rate-limiter-flexible 所取代,继续使用存在安全和维护风险。
选择 bottleneck 当你需要与第三方 API(如 GitHub、Stripe)交互,且需要处理复杂的排队逻辑、优先级调度或自动退避策略。它不仅限制并发,还能确保请求以平稳的速率发送,避免触发远程服务的 429 错误。
选择 limiter 仅当你维护遗留代码库或需要严格的令牌桶(Token Bucket)算法实现且不依赖 Promise 风格。对于新的异步优先项目,通常有更现代、更易用的替代品。
选择 p-limit 当你只需要在前端或 Node.js 脚本中简单限制同时运行的 Promise 数量(例如限制图片并发加载数),而不需要复杂的排队、存储后端或时间窗口逻辑。
选择 rate-limiter-flexible 当你需要构建高可用的分布式系统,要求限流逻辑与 Redis 等外部存储同步,或者需要在非 Express 环境(如 Koa、NestJS、纯 HTTP 服务器)中复用同一套限流规则。
express-rate-limit Basic rate-limiting middleware for Express. Use to limit repeated requests to public APIs and/or endpoints such as password reset. Plays nice with express-slow-down and ratelimit-header-parser.
The full documentation is available on-line.
import { rateLimit } from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: 'draft-8', // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive
// store: ... , // Redis, Memcached, etc. See below.
})
// Apply the rate limiting middleware to all requests.
app.use(limiter)
The rate limiter comes with a built-in memory store, and supports a variety of external data stores.
All function options may be async. Click the name for additional info and default values.
| Option | Type | Remarks |
|---|---|---|
windowMs | number | How long to remember requests for, in milliseconds. |
limit | number | function | How many requests to allow. |
message | string | json | function | Response to return after limit is reached. |
statusCode | number | HTTP status code after limit is reached (default is 429). |
handler | function | Function to run after limit is reached (overrides message and statusCode settings, if set). |
legacyHeaders | boolean | Enable the X-Rate-Limit header. |
standardHeaders | 'draft-6' | 'draft-7' | 'draft-8' | Enable the Ratelimit header. |
identifier | string | function | Name associated with the quota policy enforced by this rate limiter. |
store | Store | Use a custom store to share hit counts across multiple nodes. |
passOnStoreError | boolean | Allow (true) or block (false, default) traffic if the store becomes unavailable. |
keyGenerator | function | Identify users (defaults to IP address). |
ipv6Subnet | number (32-64) | function | false | How many bits of IPv6 addresses to use in default keyGenerator |
requestPropertyName | string | Add rate limit info to the req object. |
skip | function | Return true to bypass the limiter for the given request. |
skipSuccessfulRequests | boolean | Uncount 1xx/2xx/3xx responses. |
skipFailedRequests | boolean | Uncount 4xx/5xx responses. |
requestWasSuccessful | function | Used by skipSuccessfulRequests and skipFailedRequests. |
validate | boolean | object | Enable or disable built-in validation checks. |
logger | Logger | Custom logger |
Thanks to Mintlify for hosting the documentation at express-rate-limit.mintlify.app
And thank you to everyone who's contributed to this project in any way! 🫶
If you encounter a bug or want to see something added/changed, please go ahead and open an issue! If you need help with something, feel free to start a discussion!
If you wish to contribute to the library, thanks! First, please read the contributing guide. Then you can pick up any issue and fix/implement it!
MIT © Nathan Friedly, Vedant K