• Web Developer
  • Posts
  • How to Control the Number of Concurrent Promises in JavaScript

How to Control the Number of Concurrent Promises in JavaScript

Zero dependency, lightweight concurrent Promise limiter

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 find my content helpful, please consider subscribing. I send a weekly newsletter every Sunday with the latest web development updates. Thanks for your support!

Reply

or to participate.