Concurrency is how many operations are in flight at once: started, but not finished yet. When you have a list of async jobs, such as 200 files to upload or 50 pages to fetch, a concurrency limit of N means at most N of them run at the same time. The rest wait, and as soon as any running job finishes, the next waiting one starts in its place.
Picture a supermarket with three checkouts. However long the queue, at most three customers are served at once, and the moment any checkout is free, the next person in the queue goes to it. Nobody waits for all three to be free.
Why not start everything at once? Hundreds of requests at the same moment can overload a server or go over the number of requests it allows, and every response sits in memory at the same time. Browsers also open only a few HTTP/1.1 connections to the same host, usually six, and queue the requests beyond that. A limit keeps the load steady and under your control.
| How you run the jobs | In flight at once | What you get |
|---|---|---|
| In series: each one starts when the one before it finishes | 1 | The gentlest, and the slowest: the total time is the sum of all the jobs. It's a concurrency limit of 1. |
All at once: Promise.all(urls.map((url) => fetch(url))) | All of them | The fastest when nothing pushes back, but 500 URLs means 500 requests started at the same moment. |
| With a concurrency limit of N | At most N | Much faster than in series, while never more than N run at the same moment. |
The next job starts as soon as a slot frees up, not when a whole batch of N is done. Here are four jobs with a limit of 2, where A takes 300 ms and B, C, and D take 100 ms each:
| Time | In fixed batches of 2 | Starting whenever a slot frees up |
|---|---|---|
| 0 ms | A and B start. | A and B start. |
| 100 ms | B is done, but C waits for the rest of the batch. | B is done, so C starts. |
| 200 ms | Still waiting for A. | C is done, so D starts. |
| 300 ms | A is done, so C and D start. | A and D are done: all four have finished. |
| 400 ms | C and D are done: all four have finished. | Nothing left to do. |
In batches, B's slot sat empty from 100 to 300 ms while A finished, so the work took 400 ms instead of 300. You meet concurrency limits when uploading many files, prefetching images, or calling an API once for every item in a list; npm packages such as p-limit and p-map provide them.
Write a class TaskQueue. new TaskQueue(concurrency) creates a queue that runs async tasks, at most concurrency at a time (1 when you leave it out).
queue.add(task) adds a task: a function that returns a promise or a plain value. Tasks start in the order they were added, as soon as there's room. add returns a promise that settles like the task: it fulfills with the task's result, or rejects with its error, also when the task throws. A failing task doesn't stop the queue.
queue.pause() stops new tasks from starting; tasks already running carry on. queue.resume() starts waiting tasks again, up to the limit. queue.pending is the number of tasks waiting to start, and queue.running the number running now.
const queue = new TaskQueue(2);
const uploads = files.map((file) => queue.add(() => upload(file)));
// two uploads run at a time; the rest wait their turn
queue.pause(); // the running uploads finish, but no new ones start
queue.resume(); // carry on
const results = await Promise.all(uploads);
const queue = new TaskQueue(2);
await expect(queue.add(() => Promise.resolve("done"))).resolves.toBe("done");
await expect(queue.add(() => 42)).resolves.toBe(42);
const clock = useFakeTimers();
const queue = new TaskQueue(2);
const started = [];
const task = (name) => () => {
started.push(name);
return new Promise((resolve) => setTimeout(resolve, 100, name));
};
["a", "b", "c", "d", "e"].forEach((name) => queue.add(task(name)));
await flush();
expect(started).toEqual(["a", "b"]);
await clock.tickAsync(100);
expect(started).toEqual(["a", "b", "c", "d"]);
const clock = useFakeTimers();
const queue = new TaskQueue();
const started = [];
const task = (name) => () => {
started.push(name);
return new Promise((resolve) => setTimeout(resolve, 100, name));
};
queue.add(task("a"));
queue.add(task("b"));
await flush();
expect(started).toEqual(["a"]);
await clock.tickAsync(100);
expect(started).toEqual(["a", "b"]);
const clock = useFakeTimers();
const queue = new TaskQueue(2);
const start = clock.now();
const startedAt = [];
const task = (ms) => () => {
startedAt.push(clock.now() - start);
return new Promise((resolve) => setTimeout(resolve, ms));
};
[100, 300, 100, 100].forEach((ms) => queue.add(task(ms)));
await clock.tickAsync(1000);
expect(startedAt).toEqual([0, 0, 100, 200]);
const queue = new TaskQueue(2);
const error = new Error("upload failed");
const reasons = [];
queue.add(() => Promise.reject(error)).catch((reason) => reasons.push(reason));
queue
.add(() => {
throw error;
})
.catch((reason) => reasons.push(reason));
await flush();
expect(reasons).toHaveLength(2);
expect(reasons[0]).toBe(error);
expect(reasons[1]).toBe(error);
const queue = new TaskQueue(1);
const fail = () => {
throw new Error("upload failed");
};
queue.add(fail).catch(() => {}); // these two reject; this test only checks the task after them
queue.add(() => Promise.reject(new Error("upload failed"))).catch(() => {});
let result;
queue.add(() => "still running").then((value) => {
result = value;
});
await flush();
expect(result).toBe("still running");
const clock = useFakeTimers();
const queue = new TaskQueue(1);
const second = spy(() => "second");
let first;
queue.add(() => new Promise((resolve) => setTimeout(resolve, 100, "first"))).then((value) => {
first = value;
});
queue.add(second);
await flush();
queue.pause();
await clock.tickAsync(500);
expect(first).toBe("first");
expect(second).not.toHaveBeenCalled();
const queue = new TaskQueue(2);
queue.pause();
const tasks = [1, 2, 3].map(() => spy(() => new Promise(() => {})));
tasks.forEach((task) => queue.add(task));
await flush();
expect(tasks.map((task) => task.callCount)).toEqual([0, 0, 0]);
queue.resume();
await flush();
expect(tasks.map((task) => task.callCount)).toEqual([1, 1, 0]);
const clock = useFakeTimers();
const queue = new TaskQueue(2);
const task = () => new Promise((resolve) => setTimeout(resolve, 100));
for (let i = 0; i < 5; i++) queue.add(task);
await flush();
expect([queue.running, queue.pending]).toEqual([2, 3]);
await clock.tickAsync(100);
expect([queue.running, queue.pending]).toEqual([2, 1]);
await clock.tickAsync(100);
expect([queue.running, queue.pending]).toEqual([1, 0]);
await clock.tickAsync(100);
expect([queue.running, queue.pending]).toEqual([0, 0]);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Async task queue with pause and resume. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.