DEV Community

Cover image for How to Control the Number of Concurrent Promises in JavaScript
Zachary Lee
Zachary Lee

Posted on • Originally published at webdeveloper.beehiiv.com on

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}`)
    )
  );
})();
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 find this helpful, please consider subscribing to my newsletter for more insights on web development. Thank you for reading!

Top comments (0)