All questions

Async Task Queue

Premium

Async Task Queue

An async task queue runs jobs under a fixed concurrency limit: it starts up to concurrency tasks at once and holds the rest in a FIFO line until a slot frees up. You've hit this shape whenever a batch of uploads, API calls, or image conversions would overwhelm the network or a rate limit if fired all at once, so you cap how many run in parallel and feed the rest in as earlier ones finish.

Implement asyncTaskQueue(concurrency). It returns a queue object. Calling add(taskFn) enqueues an async task and returns a promise that settles with taskFn()'s result (or its rejection). At most concurrency tasks run at once; the rest wait in the order they were added. A task that rejects must not stop the queue — the next waiting task takes its slot. The queue also exposes onIdle(), a promise that resolves once everything (running and queued) has finished, plus live size and pending counts.

Signature

type Task<T> = () => Promise<T> | T;

interface TaskQueue {
  add<T>(task: Task<T>): Promise<T>; // enqueue; settles with task()'s outcome
  onIdle(): Promise<void>;           // resolves when running + queued both hit 0
  readonly size: number;             // tasks still waiting (not yet started)
  readonly pending: number;          // tasks running right now
}

function asyncTaskQueue(concurrency: number): TaskQueue;

Examples

const queue = asyncTaskQueue(2); // at most 2 run at once
const p1 = queue.add(() => fetchUser(1));
const p2 = queue.add(() => fetchUser(2));
const p3 = queue.add(() => fetchUser(3)); // waits for a free slot
queue.pending; // 2  (p1, p2 are running)
queue.size; // 1  (p3 is waiting)
await queue.onIdle(); // resolves after all three finish
const queue = asyncTaskQueue(1); // strictly one at a time
const a = queue.add(async () => {
  throw new Error('boom');
});
const b = queue.add(async () => 'ok');
await a.catch((e) => e.message); // 'boom'
await b; // 'ok' — b still ran, the rejection did not stall the queue

Notes

  • Concurrency cap — never run more than concurrency tasks in flight; the rest wait in FIFO order and start as slots free up.
  • Each add is independent — the promise it returns settles with that one task's own result or rejection, never another task's.
  • A rejection never stalls the queue — when a task rejects, its slot is handed to the next waiting task just as a success would.
  • size vs pendingsize counts tasks still waiting; pending counts tasks running now. onIdle() resolves when both are 0.
  • Assume concurrency is a positive integer — you don't need to validate it, change it later, or support cancellation.

FAQ

What's the difference between size and pending?
size is how many tasks are still waiting for a free slot; pending is how many are running right now. onIdle() resolves only once both reach zero.
Does one task's rejection cancel the others?
No. Each add() returns its own promise, so a rejection settles only that task's promise. The freed slot is immediately handed to the next waiting task.
How is this different from a concurrency-limited Promise.all?
Promise.all-style helpers take a fixed array up front and resolve to one ordered result array. This queue is long-lived: you can add tasks at any time and every call gets its own promise back.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium