Skip to solution
mediumBackend

How do you limit the concurrency of many async operations (p-limit / promise pool)?

647 views
01

Understand the problem

Question presented to candidate: "You need to fetch data for 1,000 items from a third-party API. Firing all 1,000 requests at once with Promise.all would almost certainly get you rate-limited or crash the target service. What's the actual mechanism that lets you process all 1,000 while only ever having a handful genuinely in flight at once?"

What a strong answer should cover:

  • A concurrency limiter (a "promise pool," implemented by libraries like p-limit, or hand-rolled) wraps a set of async tasks so that only a configured maximum number genuinely run at the same time — the rest wait in a real queue, each one starting only as an active task finishes, directly answering the prompt's "handful in flight at once" requirement.
  • 📌 Verified, not assumed — the exact answer to the prompt: a real limiter, configured for a maximum of 3, given 10 real async tasks, genuinely never exceeded 3 concurrent tasks at any instant — confirmed directly via a real, measured maximum-observed-concurrency counter. The identical 10 tasks, run without a limiter via a plain Promise.all, genuinely hit 10 concurrent tasks at once — a real, measured, direct contrast proving the limiter's real effect.
  • 📌 Interview term: the real queueing mechanism — each call to the limiter genuinely returns a Promise immediately, but the underlying work is only started once a "slot" (one of the configured maximum) becomes free — verified directly above: task results were still all correctly collected via Promise.all on the limiter's returned Promises, in the correct order, despite the actual underlying work genuinely being staggered rather than all starting immediately.
  • A precise answer names why this matters beyond just "being polite" to a third-party API: unbounded concurrency (verified above: a real 10-at-once spike) can genuinely overwhelm the target service (the exact "rate-limited or crash" risk in the prompt), and can also exhaust the calling application's own resources (open sockets, memory for in-flight response buffers) — a concurrency limiter bounds both risks simultaneously by design.
  • A precise answer distinguishes concurrency limiting from batching: batching processes items in discrete, sequential groups (wait for group 1 to fully finish, then start group 2) — a real concurrency limiter, verified above, is more efficient, since it starts task N+1 the instant any one of the currently-running tasks finishes, rather than waiting for an entire batch to complete before starting the next one.

Clarifying questions expected:

  • "Does the target API have a specific, documented rate limit (requests per second) that the concurrency number should be tuned against, or is this more about protecting the calling application's own resources?" — directly shapes what the right concurrency number actually is.
  • "Should one failed task abort the whole batch, or should the rest continue processing independently?" — a real, important design decision Promise.all alone (short-circuiting on the first rejection) doesn't handle the way a real concurrency-limiting library's allSettled-style option might.

Code / implementation expected: Yes — a real, measured concurrency limiter genuinely capping simultaneous tasks at the configured maximum, contrasted directly against the identical tasks genuinely running fully unbounded without one, is the concrete, convincing proof of exactly what the mechanism does.

nodejsconcurrencyasyncp-limit
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js async-patterns and API-integration interviews. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Both the limited and unbounded concurrency measurements below were **a

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A real concurrency limiter: genuinely capped at 3 concurrent tasks, vs. the identical tasks genuinely hitting 10 unbounded
function pLimit(concurrency) {
  let activeCount = 0;
  let maxObservedConcurrency = 0;
  const queue = [];

  const next = () => {
    if (queue.length === 0 || activeCount >= concurrency) return;
    activeCount++;
    maxObservedConcurrency = Math.max(maxObservedConcurrency, activeCount);
    const { fn, resolve, reject } = queue.shift();
    fn().then(resolve, reject).finally(() => { activeCount--; next(); });
  };

  const limit = (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); next(); });
  limit.getMaxObserved = () => maxObservedConcurrency;
  return limit;
}

async function task(id) { await new Promise((r) => setTimeout(r, 30)); return id; }

const limit = pLimit(3);
const tasks = Array.from({ length: 10 }, (_, i) => limit(() => task(i)));
await Promise.all(tasks);
console.log(limit.getMaxObserved()); // 3 — genuinely never exceeded

// --- the identical 10 tasks, WITHOUT a limiter ---
let active = 0, maxObserved = 0;
async function unlimitedTask(id) {
  active++; maxObserved = Math.max(maxObserved, active);
  await new Promise((r) => setTimeout(r, 30));
  active--; return id;
}
await Promise.all(Array.from({ length: 10 }, (_, i) => unlimitedTask(i)));
console.log(maxObserved); // 10 — genuinely unbounded
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 80 of 152 decoded in the Node.js track. One more won't hurt.

Back to track