TypeScript: running async tasks with a concurrency limit
Start at most N tasks at once, collect results in input order, and measure the peak concurrency.
On this page
TypeScript generics, async functions, and Promise.all.
- Start at most N tasks at once, collect results in input order, and measure the peak concurrency.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
Starting thousands of asynchronous jobs at once can overwhelm a server, a database, or the network. A pool limits how many run at the same time. The pool has N workers, and each worker repeatedly takes the next unstarted task, awaits it, and stores its result at the task's own index.
Because results are stored by index, the result array keeps the input order even though tasks finish in a different order. Taking next++ is safe in JavaScript because there is only one thread running the code between awaits. The program records the largest number of tasks running at once to show that the limit holds.
Code example
async function tick(times: number): Promise<void> {
for (let i = 0; i < times; i++) {
await null;
}
}
async function pool<T>(tasks: (() => Promise<T>)[], limit: number): Promise<T[]> {
const results: T[] = new Array(tasks.length);
let next = 0;
async function worker(): Promise<void> {
while (next < tasks.length) {
const index = next++;
results[index] = await tasks[index]();
}
}
await Promise.all(Array.from({ length: limit }, () => worker()));
return results;
}
let running = 0;
let peak = 0;
const finished: string[] = [];
function task(name: string, ticks: number): () => Promise<string> {
return async () => {
running++;
peak = Math.max(peak, running);
await tick(ticks);
running--;
finished.push(name);
return name.toUpperCase();
};
}
const tasks = [task("a", 10), task("b", 1), task("c", 2), task("d", 1)];
pool(tasks, 2).then((results) => {
console.log("results in input order: " + results.join(","));
console.log("finished order: " + finished.join(","));
console.log("peak concurrency: " + peak);
});results in input order: A,B,C,D finished order: b,c,d,a peak concurrency: 2
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
async function tick(times: number): Promise<void> { for (let i = 0; i < times; i++) { await null; }} async function pool<T>(tasks: (() => Promise<T>)[], limit: number): Promise<T[]> { const results: T[] = new Array(tasks.length); let next = 0; async function worker(): Promise<void> { while (next < tasks.length) { const index = next++; results[index] = await tasks[index](); } } await Promise.all(Array.from({ length: limit }, () => worker())); return results;} let running = 0;let peak = 0;const finished: string[] = []; function task(name: string, ticks: number): () => Promise<string> { return async () => { running++; peak = Math.max(peak, running); await tick(ticks); running--; finished.push(name); return name.toUpperCase(); };} const tasks = [task("a", 10), task("b", 1), task("c", 2), task("d", 1)];pool(tasks, 2).then((results) => { console.log("results in input order: " + results.join(",")); console.log("finished order: " + finished.join(",")); console.log("peak concurrency: " + peak);});Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Each worker claims the next task index. Only one piece of code runs at a time between awaits, so two workers never claim the same task. | — | — | |
| The result is stored at the task's own position. | — | — | |
| Start the fixed number of workers and wait for all to finish. | 2 workers | — | |
| Track how many tasks are running at once; it never exceeds the limit. | 2 workers | — | |
| Results follow the input order a, b, c, d. | 2 workers | results in input order: A,B,C,D | |
| Task a needs ten turns and the others only one or two, so b, c, and d finish before it. | 2 workers | results in input order: A,B,C,D finished order: b,c,d,a | |
| Never more than two tasks ran together. | 2 workers | results in input order: A,B,C,D finished order: b,c,d,a peak concurrency: 2 |
Behind the scenes
How TypeScript processes this code
TypeScript checks types and emits JavaScript. Type annotations are removed before execution.
Cost and common mistakes
O(n) tasks started; at most `limit` run at once.
- If a task rejects, Promise.all rejects at once while other workers keep going. Decide whether to stop or collect errors.
- This pool limits concurrency, not parallelism. Async tasks on one thread still take turns.
- Using a real timer such as setTimeout instead of tick makes the finishing order depend on the clock, which is realistic but less repeatable.
Try it yourself
What changes if the limit is 1?
Reveal answer and reasoning
The tasks run strictly one after another in input order, so the finished order equals the input order and the peak is 1.