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

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

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

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

3 年

GitHub Starsランキング

統計詳細

パッケージ
ダウンロード数
Stars
サイズ
Issues
公開日時
ライセンス
p-limit101,915,9922,82814.9 kB12ヶ月前MIT
limiter01,558158 kB141年前MIT
p-throttle051521.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 應用でレガシーなコードと合わせる場合に検討します。

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

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

  • p-limit:

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

  • limiter:

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

  • p-throttle:

    単位時間あたりの実行回数を厳密に制限したい場合に選択します。例えば、1 秒間に 10 回までというように、時間ベースの制限が必要な API や、ユーザー操作の頻度を落としたい場合に有効です。間隔を空けて確実に実行したいときに使います。

p-limit のREADME

p-limit

Run multiple promise-returning & async functions with limited concurrency

Works in Node.js and browsers.

Install

npm install p-limit

Usage

import pLimit from 'p-limit';

const limit = pLimit(1);

const input = [
	limit(() => fetchSomething('foo')),
	limit(() => fetchSomething('bar')),
	limit(() => doSomething())
];

// Only one promise is run at once
const result = await Promise.all(input);
console.log(result);

API

pLimit(concurrency) default export

Returns a limit function.

concurrency

Type: number | object
Minimum: 1

Concurrency limit.

You can pass a number or an options object with a concurrency property.

rejectOnClear

Type: boolean
Default: false

Reject pending promises with an AbortError when clearQueue() is called. This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().

import pLimit from 'p-limit';

const limit = pLimit({concurrency: 1});

limit(fn, ...args)

Returns the promise returned by calling fn(...args).

fn

Type: Function

Promise-returning/async function.

args

Any arguments to pass through to fn.

Support for passing arguments on to the fn is provided in order to be able to avoid creating unnecessary closures. You probably don't need this optimization unless you're pushing a lot of functions.

Warning: Avoid calling the same limit function inside a function that is already limited by it. This can create a deadlock where inner tasks never run. Use a separate limiter for inner tasks.

limit.map(iterable, mapperFunction)

Process an iterable of inputs with limited concurrency.

The mapper function receives the item value and its index.

Returns a promise equivalent to Promise.all(Array.from(iterable, (item, index) => limit(mapperFunction, item, index))).

This is a convenience function for processing inputs that arrive in batches. For more complex use cases, see p-map.

limit.activeCount

The number of promises that are currently running.

limit.pendingCount

The number of promises that are waiting to run (i.e. their internal fn was not called yet).

limit.clearQueue()

Discard pending promises that are waiting to run.

This might be useful if you want to teardown the queue at the end of your program's lifecycle or discard any function calls referencing an intermediary state of your app.

Note: This does not cancel promises that are already running.

When rejectOnClear is enabled, pending promises are rejected with an AbortError. This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().

limit.concurrency

Get or set the concurrency limit.

limitFunction(fn, options) named export

Returns a function with limited concurrency.

The returned function manages its own concurrent executions, allowing you to call it multiple times without exceeding the specified concurrency limit.

Ideal for scenarios where you need to control the number of simultaneous executions of a single function, rather than managing concurrency across multiple functions.

import {limitFunction} from 'p-limit';

const limitedFunction = limitFunction(async () => {
	return doSomething();
}, {concurrency: 1});

const input = Array.from({length: 10}, limitedFunction);

// Only one promise is run at once.
await Promise.all(input);

fn

Type: Function

Promise-returning/async function.

options

Type: object

concurrency

Type: number
Minimum: 1

Concurrency limit.

rejectOnClear

Type: boolean
Default: false

Reject pending promises with an AbortError when clearQueue() is called. This is recommended if you await the returned promises, for example with Promise.all, so pending tasks do not remain unresolved after clearQueue().

Recipes

See recipes.md for common use cases and patterns.

FAQ

How is this different from the p-queue package?

This package is only about limiting the number of concurrent executions, while p-queue is a fully featured queue implementation with lots of different options, introspection, and ability to pause the queue.

Related

  • p-throttle - Throttle promise-returning & async functions
  • p-debounce - Debounce promise-returning & async functions
  • p-map - Run promise-returning & async functions concurrently with different inputs
  • p-all - Run promise-returning & async functions concurrently with optional limited concurrency
  • More…