テーマの整理:「件数制御実行」とは何か
「件数制御実行」は、配列の要素に対する非同期処理を「一度に何件まで動かすか」をきちんと制御しながら実行するユーティリティです。 イメージとしては「同時実行数を制限した並列処理」ですが、業務ではほぼ必須レベルの考え方になります。
API を 1000 件分叩きたい。 ファイルを大量にアップロードしたい。 外部サービスに対して大量のチェック処理を投げたい。
こういうときに「全部一気に並列で実行」すると、レート制限や負荷で簡単に死にます。 そこで、「同時に動かす件数を制御する」ユーティリティが必要になります。
基本コンセプト:タスク配列+同時実行数 limit
タスクを「まだ実行していない関数」として持つ
件数制御実行のポイントは、「Promise そのもの」ではなく「Promise を返す関数」を配列にすることです。
const tasks = ids.map((id) => () => fetchUser(id));
JavaScriptこの tasks は、「呼び出すと非同期処理が始まる関数」の配列です。 まだ実行していないので、いつ・何個・どの順番で動かすかをこちらで決められます。
limit で「同時に動かす最大件数」を決める
次に、limit という数値で「同時に動かしてよい最大件数」を決めます。
例えば limit = 3 なら、「常に最大 3 個までしか同時に動かさない」ように制御します。 これが「件数制御実行」の中核です。
実装例:runWithLimit(件数制御実行ユーティリティ)
コード全体
async function runWithLimit(tasks, limit = 5) {
if (!Array.isArray(tasks)) {
return [];
}
if (typeof limit !== "number" || limit <= 0) {
limit = 1;
}
const results = new Array(tasks.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < tasks.length) {
const index = currentIndex;
currentIndex += 1;
const task = tasks[index];
try {
results[index] = await task();
} catch (e) {
results[index] = e;
}
}
}
const workers = [];
const workerCount = Math.min(limit, tasks.length);
for (let i = 0; i < workerCount; i++) {
workers.push(worker());
}
await Promise.all(workers);
return results;
}
JavaScript重要ポイントをかみ砕いて説明
tasks は「非同期処理を返す関数」の配列です。 results は「各タスクの結果(成功なら値、失敗ならエラー)」を入れる配列です。 currentIndex は「次に実行すべきタスクの位置」を共有するカウンタです。
worker は「タスクを取りに行って実行する役割」を持つ async 関数です。 while ループの中で、currentIndex を 1 つ進めて、その位置のタスクを実行します。 タスクが成功したら結果を results[index] に入れ、失敗したらエラーをそのまま入れます。
最後に、worker を limit 個だけ立ち上げて Promise.all で全部の worker が終わるのを待ちます。 これで、「常に最大 limit 個までしか同時にタスクが動かない」状態が実現されます。
例題 1:ユーザー API を最大 3 並列で叩く
シナリオ
ユーザー ID がたくさんある。 API を叩いてユーザー情報を取得したいが、一度に叩きすぎるとレート制限が怖い。 そこで「最大 3 並列」で安全に実行したい。
コード
async function fetchUser(id) {
console.log("start", id);
await new Promise((r) => setTimeout(r, 500));
console.log("end", id);
return { id, name: `User-${id}` };
}
async function main() {
const ids = [1, 2, 3, 4, 5, 6];
const tasks = ids.map((id) => () => fetchUser(id));
const results = await runWithLimit(tasks, 3);
console.log(results);
}
main();
JavaScriptログを眺めると、 「常に 3 件までしか start が並んでいない」ことが分かるはずです。 これがまさに「件数制御実行」です。
例題 2:ファイルアップロードを帯域を守りながら実行する
シナリオ
10 個のファイルをアップロードしたい。 全部同時にアップロードすると回線がパンクするので、最大 2 個ずつにしたい。
コード
async function uploadFile(name) {
console.log("upload start", name);
await new Promise((r) => setTimeout(r, 800));
console.log("upload end", name);
return { name, status: "ok" };
}
async function main() {
const files = ["a.txt", "b.txt", "c.txt", "d.txt"];
const tasks = files.map((name) => () => uploadFile(name));
const results = await runWithLimit(tasks, 2);
console.log(results);
}
main();
JavaScriptここでも、「常に 2 個までしかアップロードが同時に走っていない」ことがログで確認できます。 ネットワーク帯域やストレージ負荷を守りながら処理できる、典型的な件数制御実行の使い方です。
例題 3:失敗も結果として集める(後で再試行したいケース)
シナリオ
タスクの中には失敗するものもある。 失敗したものは後で再試行したいので、「どれが失敗したか」を結果として残したい。
コード
async function maybeFailTask(id) {
console.log("task start", id);
await new Promise((r) => setTimeout(r, 300));
console.log("task end", id);
if (id % 2 === 0) {
throw new Error("failed: " + id);
}
return { id, ok: true };
}
async function main() {
const ids = [1, 2, 3, 4];
const tasks = ids.map((id) => () => maybeFailTask(id));
const results = await runWithLimit(tasks, 2);
const success = results.filter((r) => !(r instanceof Error));
const failed = results.filter((r) => r instanceof Error);
console.log("success:", success);
console.log("failed:", failed);
}
main();
JavaScriptrunWithLimit の中で try/catch しているので、 成功したタスクは値、失敗したタスクは Error オブジェクトとして結果配列に残ります。 これを使って「失敗したものだけ再試行する」などのロジックを組み立てられます。
件数制御実行で意識してほしい重要ポイント
「同時に何件までなら安全か?」を決める
limit の値は、なんとなく決めるのではなく、 API のレート制限、外部サービスの推奨値、ネットワーク帯域、サーバー負荷などを考えて決めるのが理想です。
例えば、
社内 API なら 10 並列でも平気かもしれない。 外部の課金 API なら 2 並列くらいに抑えたいかもしれない。
こういう「現実世界の事情」を limit に反映させるのが、業務ユーティリティとしての大事な設計です。
「タスクを関数として持つ」ことの意味
Promise を直接配列にしてしまうと、「作った瞬間に実行が始まる」ので制御しづらくなります。 一方、「Promise を返す関数」を配列にしておけば、 「いつ実行するか」「何個同時に実行するか」をこちらで決められます。
この「タスク=関数」という発想は、 非同期処理をちゃんとコントロールするうえで、とても重要な考え方です。
戻り値は必ず Promise になる
runWithLimit は async function なので、戻り値は必ず Promise です。 呼び出し側は必ず await runWithLimit(...) と書く必要があります。
ここを忘れて「配列だと思っていたら Promise だった」というのは、非同期あるあるなので、 「件数制御実行ユーティリティ=必ず await」とセットで覚えておくと安全です。
まとめ:件数制御実行ユーティリティで「安全な並列」を標準化する
「件数制御実行」は、 単に便利な関数というより、「非同期処理を安全に並列化するための設計そのもの」です。
プロジェクトに例えば次のようなユーティリティを置いておくイメージです。
export async function runWithLimit(tasks, limit) { ... }
JavaScriptそして、「大量の非同期処理を配列で扱うときは、必ず runWithLimit を通す」と決めておく。 それだけで、レート制限や負荷の事故が減り、 「どこまで並列にしてよくて、どこからは抑えるべきか」がチームで共有しやすくなります。
配列ユーティリティとしての件数制御実行は、 “速さ”ではなく“安全さ”を優先した並列処理のための、かなり実務的な武器です。
