Question presented to candidate: "You have a list of async jobs, say a hundred URLs to fetch, but you cannot run all of them at once without overwhelming the downstream service or the browser's own connection limits. Implement a task queue that runs at most N of them concurrently, starts a new one the instant a slot frees up, and still gives the caller back every result once everything finishes."
What a strong answer should cover:
- A queue holds pending tasks (functions that return a promise) plus a running count; it only starts a new task when
running < concurrency, and starts one MORE the instant a task finishes — not in a fixed batch. - 📌 Interview term: a semaphore pattern — the running counter, incremented when a task starts and decremented when it settles, is exactly a semaphore controlling how many tasks may be "in flight" at once.
- Each call to add a task should return its OWN promise that resolves (or rejects) with that specific task's own outcome — the caller needs its results correlated to its own individual tasks, not just "the batch finished."
- A rejected task must not stop the queue or block sibling tasks — a real, robust queue isolates failures per task, unlike a naive
Promise.all, which stops at the first rejection. - A precise answer distinguishes this from two naive alternatives and their real trade-offs:
Promise.all(unlimited concurrency, all at once) and a sequentialfor-loop withawait(concurrency of exactly 1, needlessly serial). - Results should come back in the CALLER's original order, not completion order, since
Promise.allover the returned promises already gives this for free as long as each task's own promise is returned immediately when queued.
Clarifying questions expected:
- "Should a task be allowed to add MORE tasks to the same queue while it is running?" — affects whether the internal queue array can safely be mutated mid-iteration.
- "If one task fails, should the whole batch fail fast, or should the caller get every result (success and failure) individually?" — changes whether
Promise.allorPromise.allSettledis the right tool for the caller's own aggregation. - "Is the concurrency limit fixed for the queue's lifetime, or does it need to change at runtime?"
Code / implementation expected: Yes — a real, runnable implementation, executed with a real running-concurrency counter and a real rejection-isolation test, is the concrete way to prove the limit is actually enforced and failures do not cascade.