limiter vs p-limit vs p-throttle
JavaScript 任务并发与速率限制方案对比
limiterp-limitp-throttle类似的npm包:

JavaScript 任务并发与速率限制方案对比

这三个库都用于控制代码执行的频率和并发量,但核心机制不同。p-limit 用于限制同时运行的任务数量(并发度);p-throttle 用于限制单位时间内的调用次数(速率);limiter 提供基于令牌桶算法的底层限流,更偏向 Node.js 服务端场景。前端开发中通常首选 p-* 系列以获得更好的 Promise 支持。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
limiter01,560158 kB151 年前MIT
p-limit02,86414.9 kB04 个月前MIT
p-throttle051821.6 kB06 个月前MIT

JavaScript 任务并发与速率限制方案对比

在前端和 Node.js 开发中,控制任务执行的速度和数量是常见需求。limiterp-limitp-throttle 都解决了这个问题,但它们的适用场景和实现方式有很大区别。让我们从实际工程角度看看它们如何工作。

🚀 核心机制:并发数 vs 速率 vs 令牌桶

p-limit 关注的是同时有多少个任务在运行

  • 它限制的是并发度(Concurrency)。
  • 即使你有 100 个任务,如果限制为 2,那么同一时刻只有 2 个在执行,其余排队。
// p-limit: 限制同时运行 2 个任务
import pLimit from 'p-limit';
const limit = pLimit(2);

const jobs = Array.from({ length: 5 }).map(() => 
  limit(() => fetch('/api/data'))
);
await Promise.all(jobs);
// 同一时刻最多 2 个 fetch 在进行

p-throttle 关注的是单位时间内允许通过多少个任务

  • 它限制的是速率(Rate)。
  • 即使并发只有 1 个,如果限制每秒 2 次,第 3 次调用会被延迟到下一秒。
// p-throttle: 限制每秒最多 2 次调用
import pThrottle from 'p-throttle';
const throttle = pThrottle({limit: 2, interval: 1000});

const throttledFetch = throttle(() => fetch('/api/data'));
await throttledFetch();
await throttledFetch();
await throttledFetch(); // 这次会被延迟到下一个时间窗口

limiter 关注的是令牌桶算法

  • 它维护一个令牌池,每次执行消耗令牌,令牌随时间自动补充。
  • 更灵活,但配置更复杂,通常用于服务端限流。
// limiter: 每秒生成 2 个令牌,桶容量 2
const { RateLimiter } = require('limiter');
const limiter = new RateLimiter(2, 'second');

limiter.removeTokens(1, (err, remaining) => {
  // 拿到令牌后才能执行请求
  fetch('/api/data');
});

📦 Promise 支持:现代异步流程的关键

对于前端开发者来说,是否原生支持 Promise 是选择的重要标准。

p-limit 完全基于 Promise 设计。

  • 返回的是一个 Promise。
  • 可以直接配合 async/await 使用,代码清晰。
// p-limit: 原生 Promise 支持
async function run() {
  const limit = pLimit(2);
  await limit(() => doSomething());
}

p-throttle 同样完全基于 Promise 设计。

  • 包装后的函数返回 Promise。
  • 错误处理可以直接用 try/catch。
// p-throttle: 原生 Promise 支持
async function run() {
  const throttle = pThrottle({limit: 1, interval: 1000});
  try {
    await throttle(() => doSomething());
  } catch (e) {
    // 直接捕获错误
  }
}

limiter 主要基于回调函数(Callback)。

  • 默认 API 使用回调,不支持直接 await。
  • 在现代 async/await 代码中需要额外包装,增加复杂度。
// limiter: 主要基于回调
function run() {
  limiter.removeTokens(1, (err, remaining) => {
    // 需要在回调里继续逻辑
    // 如果要转 Promise 需手动封装
  });
}

⚠️ 错误处理与排队行为

当任务失败或队列满时,各包的表现不同。

p-limit 会保留所有任务直到执行。

  • 任务会排队等待,不会因为并发满而丢弃。
  • 如果某个任务报错,不影响其他任务。
// p-limit: 任务排队,错误隔离
const limit = pLimit(2);
// 即使并发满,新任务也会进入队列等待
await limit(() => riskyOperation()); 

p-throttle 会延迟执行直到时间窗口允许。

  • 任务不会丢失,只是被推迟。
  • 适合需要保证最终执行但不在乎稍慢的场景。
// p-throttle: 自动延迟
const throttle = pThrottle({limit: 1, interval: 1000});
// 调用过快会自动排队延迟执行
await throttle(() => apiCall()); 

limiter 取决于你如何处理回调。

  • 如果没有令牌,回调会等待直到有令牌。
  • 需要手动处理等待逻辑,不如 p-* 系列自动化。
// limiter: 手动处理等待
limiter.removeTokens(1, (err, remaining) => {
  // 如果没有令牌,回调会阻塞直到可用
  // 需要自己处理超时或放弃逻辑
});

🌐 真实场景推荐

场景 1:批量上传图片

你需要上传 50 张图片,但浏览器同时连接数有限,且不想卡死界面。

  • 最佳选择p-limit
  • 理由:限制同时上传数量(如 5 个),避免占用所有网络资源。
// 使用 p-limit
const limit = pLimit(5);
const uploads = images.map(img => limit(() => upload(img)));
await Promise.all(uploads);

场景 2:调用有限制的 API

第三方 API 规定每秒最多调用 10 次,否则会返回 429 错误。

  • 最佳选择p-throttle
  • 理由:严格控制时间窗口内的请求次数,避免触发限流。
// 使用 p-throttle
const throttle = pThrottle({limit: 10, interval: 1000});
const callApi = throttle(() => fetch('/external-api'));
await callApi();

场景 3:Node.js 服务端网关

你在编写一个 API 网关,需要根据用户等级分配不同的令牌速率。

  • 最佳选择limiter
  • 理由:令牌桶算法更适合复杂的服务端流控策略。
// 使用 limiter
const userLimiter = new RateLimiter(userTokens, 'second');
userLimiter.removeTokens(1, callback);

📊 核心差异总结

特性p-limitp-throttlelimiter
控制目标同时运行的任务数单位时间内的调用次数令牌消耗速率
异步风格Promise / async-awaitPromise / async-await回调函数 (Callback)
主要场景并发控制 (上传/下载)速率限制 (API 调用)服务端流控 (网关)
前端友好度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
配置复杂度低 (仅并发数)中 (次数 + 时间)高 (令牌 + 间隔)

💡 最终建议

对于现代前端开发,优先选择 p-limitp-throttle

  • 如果你的问题是**“太多任务同时跑会卡死”**,用 p-limit
  • 如果你的问题是**“请求太快会被服务器拒绝”**,用 p-throttle
  • 除非你在 Node.js 服务端需要复杂的令牌桶逻辑,否则不建议在前端使用 limiter,因为它的回调风格会增加代码维护成本,且不如 p-* 系列对 Promise 友好。

这三个工具都很小巧,但它们解决的是不同维度的问题。选对工具能让你的异步代码更稳定,也更易读。

如何选择: limiter vs p-limit vs p-throttle

  • limiter:

    选择 limiter 如果你在 Node.js 服务端需要实现复杂的令牌桶算法,或者需要精细控制令牌生成速率。由于它主要基于回调函数,不太适合现代前端基于 async/await 的流程,新项目建议优先考虑其他方案。

  • p-limit:

    选择 p-limit 如果你需要限制同时执行的任务数量,例如同时只允许 5 个文件上传或 3 个 API 请求。它非常适合控制并发度,防止浏览器或服务器因过多同时请求而过载。

  • p-throttle:

    选择 p-throttle 如果你需要限制单位时间内的调用频率,例如每秒最多调用 10 次 API。它适合应对有严格速率限制的第三方接口,确保不会因请求过快而被封禁。

limiter的README

limiter

Build Status NPM Downloads

Provides a generic rate limiter for the web and node.js. Useful for API clients, web crawling, or other tasks that need to be throttled. Two classes are exposed, RateLimiter and TokenBucket. TokenBucket provides a lower level interface to rate limiting with a configurable burst rate and drip rate. RateLimiter sits on top of the token bucket and adds a restriction on the maximum number of tokens that can be removed each interval to comply with common API restrictions such as "150 requests per hour maximum".

Installation

yarn add limiter

Usage

A simple example allowing 150 requests per hour:

import { RateLimiter } from "limiter";

// Allow 150 requests per hour (the Twitter search limit). Also understands
// 'second', 'minute', 'day', or a number of milliseconds
const limiter = new RateLimiter({ tokensPerInterval: 150, interval: "hour" });

async function sendRequest() {
  // This call will throw if we request more than the maximum number of requests
  // that were set in the constructor
  // remainingRequests tells us how many additional requests could be sent
  // right this moment
  const remainingRequests = await limiter.removeTokens(1);
  callMyRequestSendingFunction(...);
}

Another example allowing one message to be sent every 250ms:

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 250 });

async function sendMessage() {
  const remainingMessages = await limiter.removeTokens(1);
  callMyMessageSendingFunction(...);
}

The default behaviour is to wait for the duration of the rate limiting that's currently in effect before the promise is resolved, but if you pass in "fireImmediately": true, the promise will be resolved immediately with remainingRequests set to -1:

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({
  tokensPerInterval: 150,
  interval: "hour",
  fireImmediately: true
});

async function requestHandler(request, response) {
  // Immediately send 429 header to client when rate limiting is in effect
  const remainingRequests = await limiter.removeTokens(1);
  if (remainingRequests < 0) {
    response.writeHead(429, {'Content-Type': 'text/plain;charset=UTF-8'});
    response.end('429 Too Many Requests - your IP is being rate limited');
  } else {
    callMyMessageSendingFunction(...);
  }
}

A synchronous method, tryRemoveTokens(), is available in both RateLimiter and TokenBucket. This will return immediately with a boolean value indicating if the token removal was successful.

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });

if (limiter.tryRemoveTokens(5))
  console.log('Tokens removed');
else
  console.log('No tokens removed');

To get the number of remaining tokens outside the removeTokens promise, simply use the getTokensRemaining method.

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 250 });

// Prints 1 since we did not remove a token and our number of tokens per
// interval is 1
console.log(limiter.getTokensRemaining());

Using the token bucket directly to throttle at the byte level:

import { TokenBucket } from "limiter";

const BURST_RATE = 1024 * 1024 * 150; // 150KB/sec burst rate
const FILL_RATE = 1024 * 1024 * 50; // 50KB/sec sustained rate

// We could also pass a parent token bucket in to create a hierarchical token
// bucket
// bucketSize, tokensPerInterval, interval
const bucket = new TokenBucket({
  bucketSize: BURST_RATE,
  tokensPerInterval: FILL_RATE,
  interval: "second"
});

async function handleData(myData) {
  await bucket.removeTokens(myData.byteLength);
  sendMyData(myData);
}

Additional Notes

Both the token bucket and rate limiter should be used with a message queue or some way of preventing multiple simultaneous calls to removeTokens(). Otherwise, earlier messages may get held up for long periods of time if more recent messages are continually draining the token bucket. This can lead to out of order messages or the appearance of "lost" messages under heavy load.

License

MIT License