Skip to the solution
HardSystem Design8 min read

Implement an async task queue with a concurrency limit.

1.1k views

The problem

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 sequential for-loop with await (concurrency of exactly 1, needlessly serial).
  • Results should come back in the CALLER's original order, not completion order, since Promise.all over 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.all or Promise.allSettled is 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.

  • promises
  • concurrency
  • queue

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 8 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

JavaScriptA real, runnable demo: the TaskQueue implementation, the concurrency-limit proof, and the rejection-isolation proof (identical to this doc's verified output)

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 126 of 165 in JavaScript

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

What does yield* do in a generator?

Continue