limiter vs p-limit vs p-throttle
JavaScript における非同期処理の並行制御とレート制限
limiterp-limitp-throttle類似パッケージ:

JavaScript における非同期処理の並行制御とレート制限

p-limitp-throttlelimiter は、JavaScript 應用における非同期タスクの実行頻度や同時実行数を制御するためのライブラリです。p-limit は同時に実行できるタスクの数を制限し、p-throttle は単位時間あたりの実行回数を制限します。limiter はトークンバケットアルゴリズムを用いて、より柔軟なレート制限を実現します。これらは API 呼び出しの制限や、ブラウザの負荷軽減など、さまざまな場面で活用されます。

npmのダウンロードトレンド

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
limiter01,555158 kB151年前MIT
p-limit02,83814.9 kB12ヶ月前MIT
p-throttle051721.6 kB05ヶ月前MIT

非同期処理の制御:p-limit、p-throttle、limiter の比較

JavaScript で非同期処理を扱う際、すべてのタスクを同時に実行すると、サーバーに負荷をかけたり、ブラウザが凍結したりするリスクがあります。p-limitp-throttlelimiter は、こうした問題を解決するためのツールですが、それぞれ制御の仕組みと目的が異なります。ここでは、実務でどのように使い分けるかを深掘りします。

🚀 制御の仕組み:同時実行数 vs 時間ベースの制限

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-limitp-throttle は現代の Promise/async-await コードに自然にフィットします。limiter は古いスタイルの API なので、ラップ処理が必要になることがあります。

⚠️ エラーハンドリングの挙動

すべてのパッケージで、ラップされた関数自体がエラーを投げた場合、それは通常通り伝播します。

p-limitp-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 => {
    // ここでエラー処理
  });
});

🌐 実務でのシナリオ

シナリオ 1: 画像の批量アップロード

100 枚の画像をアップロードしたいが、サーバーに負荷をかけたくない。

  • 最佳選択: p-limit
  • 理由:同時接続数を制限すればよく、時間あたりの制限は厳密でなくてよい場合が多いからです。
const limit = pLimit(5);
const uploads = images.map(img => limit(() => upload(img)));
await Promise.all(uploads);

シナリオ 2: 外部 API の利用制限遵守

無料枠の API を利用しており、1 秒間に 10 リクエストまでという制限がある。

  • 最佳選択: p-throttle
  • 理由:時間あたりの回数を厳密に守る必要があるためです。
const throttle = pThrottle({ limit: 10, interval: 1000 });
const callApi = throttle(() => fetch('https://api.example.com'));

シナリオ 3: トラフィックのバースト制御

瞬間的なアクセス増は許容しつつ、長期的な平均レートを守りたい。

  • 最佳選択: limiter
  • 理由:トークンバケットは、一時的な増加を吸収しつつ、持続的なレート制限をかけるのに適しています。
const limiter = new RateLimiter(10, 'second');
// バースト時にトークンを消費し、落ち着くと回復する

🌱 使わないほうがよい場合

これらのライブラリは便利ですが、以下のような場合は不要です。

  • 単純な直列処理: for...of ループと await で十分な場合があります。
  • ブラウザのネイティブ機能: requestAnimationFramedebounce 関数で済む UI 制御もあります。
  • サーバー側のミドルウェア: Express や Koa 用のレート制限ミドルウェアを使うほうが適切な場合もあります。

📌 比較サマリーテーブル

機能p-limitp-throttlelimiter
制御基準同時実行数時間あたりの回数トークンバケット
主な用途並列処理の制限API レート制限柔軟なレート制限
API スタイルPromise / async-awaitPromise / async-awaitコールバック (主流)
バースト対応キューで待機間隔を空けるトークン次第で可能
依存関係ほぼなしほぼなしなし

💡 最終的な推奨事項

選択の基準は、「何を制限したいか」 です。

  • 同時接続数を減らしたいなら → p-limit
  • 時間あたりの回数を守りたいなら → p-throttle
  • 複雑なレート制御が必要なら → limiter

現代のフロントエンド開発では、p-limitp-throttle のほうが Promise との相性が良く、コードも読みやすいため、まずこれらを検討するのが一般的です。limiter は、特定のアルゴリズム要件がある場合や、サーバーサイドの Node.js 應用でレガシーなコードと合わせる場合に検討します。

これらのツールを適切に使うことで、應用の安定性とユーザー体験を向上させられます。

選び方: limiter vs p-limit vs p-throttle

  • limiter:

    トークンバケットアルゴリズムに基づいた制御が必要な場合に選択します。バーストトラフィックを許容しつつ、長期的なレート制限をかけたい場合など、より複雑な制御ロジックが必要なサーバーサイドや特定のクライアント用途に適しています。

  • p-limit:

    同時に実行するタスクの数を制限したい場合に選択します。例えば、同時に 5 つまでしか API リクエストを送信したくない場合や、リソースを消費する処理を並列化しすぎないようにする場合に適しています。実装がシンプルで、Promise ベースの処理に自然に組み込めます。

  • p-throttle:

    単位時間あたりの実行回数を厳密に制限したい場合に選択します。例えば、1 秒間に 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