p-limit、p-throttle、limiter は、JavaScript 應用における非同期タスクの実行頻度や同時実行数を制御するためのライブラリです。p-limit は同時に実行できるタスクの数を制限し、p-throttle は単位時間あたりの実行回数を制限します。limiter はトークンバケットアルゴリズムを用いて、より柔軟なレート制限を実現します。これらは API 呼び出しの制限や、ブラウザの負荷軽減など、さまざまな場面で活用されます。
JavaScript で非同期処理を扱う際、すべてのタスクを同時に実行すると、サーバーに負荷をかけたり、ブラウザが凍結したりするリスクがあります。p-limit、p-throttle、limiter は、こうした問題を解決するためのツールですが、それぞれ制御の仕組みと目的が異なります。ここでは、実務でどのように使い分けるかを深掘りします。
p-limit は、同時に実行できるプロミスの数を制限します。
import pLimit from 'p-limit';
const limit = pLimit(2); // 同時に 2 つまで
const tasks = [1, 2, 3, 4].map(id =>
limit(() => fetch(`/api/data/${id}`))
);
await Promise.all(tasks);
// 1, 2 が実行され、どちらか完了後に 3 が始まる
p-throttle は、単位時間あたりの実行回数を制限します。
import pThrottle from 'p-throttle';
const throttle = pThrottle({
limit: 2,
interval: 1000 // 1 秒間に 2 回まで
});
const throttledFetch = throttle(() => fetch('/api/data'));
await throttledFetch();
await throttledFetch();
await throttledFetch(); // 1 秒経過するまで待機される
limiter は、トークンバケットアルゴリズムを使用します。
const RateLimiter = require('limiter').RateLimiter;
// 1 秒間に 2 トークン補充、最大 2 個保持
const limiter = new RateLimiter(2, 'second');
limiter.removeTokens(1, (err, remaining) => {
// トークンがあれば即時実行、なければ待機
fetch('/api/data');
});
パッケージによって、関数のラップ方法と戻り値が異なります。
p-limit は、関数をラップして新しい関数を返します。このラップ関数は Promise を返します。p-throttle も同様に関数をラップしますが、時間間隔を内部で管理します。limiter は、クラスインスタンスを作成し、メソッドを呼び出してトークンを取得します。コールバックベースの API が主流ですが、Promise でラップして使うことも多いです。💡 ヒント:
p-limitとp-throttleは現代の Promise/async-await コードに自然にフィットします。limiterは古いスタイルの API なので、ラップ処理が必要になることがあります。
すべてのパッケージで、ラップされた関数自体がエラーを投げた場合、それは通常通り伝播します。
p-limit と p-throttle:
Promise.all などと組み合わせることで、全体の成功・失敗を制御できます。// p-limit の場合
const limit = pLimit(2);
try {
await limit(() => riskyOperation());
} catch (error) {
// エラーをキャッチ可能
}
limiter:
// limiter の場合
limiter.removeTokens(1, (err, remaining) => {
if (err) return;
riskyOperation().catch(err => {
// ここでエラー処理
});
});
100 枚の画像をアップロードしたいが、サーバーに負荷をかけたくない。
p-limitconst limit = pLimit(5);
const uploads = images.map(img => limit(() => upload(img)));
await Promise.all(uploads);
無料枠の API を利用しており、1 秒間に 10 リクエストまでという制限がある。
p-throttleconst throttle = pThrottle({ limit: 10, interval: 1000 });
const callApi = throttle(() => fetch('https://api.example.com'));
瞬間的なアクセス増は許容しつつ、長期的な平均レートを守りたい。
limiterconst limiter = new RateLimiter(10, 'second');
// バースト時にトークンを消費し、落ち着くと回復する
これらのライブラリは便利ですが、以下のような場合は不要です。
for...of ループと await で十分な場合があります。requestAnimationFrame や debounce 関数で済む UI 制御もあります。| 機能 | p-limit | p-throttle | limiter |
|---|---|---|---|
| 制御基準 | 同時実行数 | 時間あたりの回数 | トークンバケット |
| 主な用途 | 並列処理の制限 | API レート制限 | 柔軟なレート制限 |
| API スタイル | Promise / async-await | Promise / async-await | コールバック (主流) |
| バースト対応 | キューで待機 | 間隔を空ける | トークン次第で可能 |
| 依存関係 | ほぼなし | ほぼなし | なし |
選択の基準は、「何を制限したいか」 です。
p-limitp-throttlelimiter現代のフロントエンド開発では、p-limit と p-throttle のほうが Promise との相性が良く、コードも読みやすいため、まずこれらを検討するのが一般的です。limiter は、特定のアルゴリズム要件がある場合や、サーバーサイドの Node.js 應用でレガシーなコードと合わせる場合に検討します。
これらのツールを適切に使うことで、應用の安定性とユーザー体験を向上させられます。
トークンバケットアルゴリズムに基づいた制御が必要な場合に選択します。バーストトラフィックを許容しつつ、長期的なレート制限をかけたい場合など、より複雑な制御ロジックが必要なサーバーサイドや特定のクライアント用途に適しています。
同時に実行するタスクの数を制限したい場合に選択します。例えば、同時に 5 つまでしか API リクエストを送信したくない場合や、リソースを消費する処理を並列化しすぎないようにする場合に適しています。実装がシンプルで、Promise ベースの処理に自然に組み込めます。
単位時間あたりの実行回数を厳密に制限したい場合に選択します。例えば、1 秒間に 10 回までというように、時間ベースの制限が必要な API や、ユーザー操作の頻度を落としたい場合に有効です。間隔を空けて確実に実行したいときに使います。
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".
yarn add limiter
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);
}
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.
MIT License