How to Control the Number of Concurrent Promises in JavaScript
In some cases, we may need to control the number of concurrent requests.
For example, when we are writing download or crawling tools, some websites may have a limit on the number of concurrent requests.
In browsers, the maximum number of TCP connections from the same origin is limited to 6. This means that if you use HTTP1.1 when sending more than 6 requests at the same time, the 7th request will wait until the previous one is processed before it starts.
We can use the following simple example to test it. First the client code:
void (async () => {
await Promise.all(
[...new Array(12)].map((_, i) =>
fetch(`http://127.0.0.1:3001/get/${i + 1}`),
),
);
})();
Next is the brief code for the service:
router.get('/get/:id', async (ctx) => {
const order = Number(ctx.params.id);
if (order % 2 === 0 && order <= 6) {
await sleep(1000);
}
await sleep(1000);
ctx.body = 1;
});
In the first 6 requests, if the order is even, then wait for 2s, if not, wait for 1s. Then open the Network in DevTools and you can get the following picture:
Looking at the Time and Waterfall columns show that it is a model of the request concurrency limit. So I want to achieve a similar function, how to do it? I open-sourced p-limiter on Github. Here’s the same Gist:
class Queue<T> {
#tasks: T[] = [];
enqueue = (task: T) => {
this.#tasks.push(task);
};
dequeue = () => this.#tasks.shift();
clear = () => {
this.#tasks = [];
};
get size() {
return this.#tasks.length;
}
}
class PromiseLimiter {
#queue = new Queue<() => Promise<void>>();
#runningCount = 0;
#limitCount: number;
constructor(limitCount: number) {
this.#limitCount = limitCount;
}
#next = () => {
if (this.#runningCount < this.#limitCount && this.#queue.size > 0) {
this.#queue.dequeue()?.();
}
};
#run = async <R = any>(
fn: () => Promise<R>,
resolve: (value: PromiseLike<R>) => void,
) => {
this.#runningCount += 1;
const result = (async () => fn())();
resolve(result);
try {
await result;
} catch {
// ignore
}
this.#runningCount -= 1;
this.#next();
};
get activeCount() {
return this.#runningCount;
}
get pendingCount() {
return this.#queue.size;
}
limit = <R = any>(fn: () => Promise<R>) =>
new Promise<R>((resolve) => {
this.#queue.enqueue(() => this.#run(fn, resolve));
this.#next();
});
}
export default PromiseLimiter;
This can be achieved in less than 70 lines of code above, you can try it out in StackBlitz.
You can see that it works as expected. Without resorting to any third-party libraries, the simple code model is just that.
So do you have any other use cases?
If you found this helpful, consider subscribing to my newsletter for weekly web development insights and updates. Thanks for reading!
How Server-Sent Events (SSE) Work
Complete guide to Server-Sent Events (SSE) for real-time web communication. Learn SSE implementation, benefits, use cases, and how it compares to WebSockets and polling.
Ditch dotenv - Node.js Now Natively Supports .env File
Replace dotenv with Node.js native .env file support. Learn --env-file flag, process.loadEnvFile(), and util.parseEnv() methods. Master environment variable management without external dependencies.