Concurrency Patterns
The Gap Between async/await and Real Concurrency
async/await makes asynchronous code readable. It does not make it concurrent. If you await each operation in sequence, you are doing one thing at a time — the rest of the time the CPU is idle, waiting.
Sequential: ~400ms. Parallel: ~80ms. Five times faster for the same work. But Promise.all with no limits fires everything simultaneously — fine for five requests, catastrophic for five hundred.
Promise Combinators — The Full Set
Promise.all is one of four combinators. Each answers a different question about a group of promises:
The decision tree: use all when every result is required, allSettled when you need every outcome regardless of failure, race for timeouts and first-response wins, any for redundant sources where one success is enough.
Semaphore — Limiting Concurrency
A semaphore is a concurrency primitive that allows at most N operations to run simultaneously. Every additional task waits until a slot opens:
Without the semaphore all 10 tasks would fire immediately. With it, at most 3 run at any moment. This is the correct pattern when hitting external APIs with rate limits, writing to a database connection pool, or processing files with limited I/O bandwidth.
Rate Limiter — Requests Per Time Window
A semaphore limits concurrency. A rate limiter limits throughput — N operations per unit of time, regardless of how fast each one completes:
Watch the output: the first 5 fire immediately, then the next batch waits for the window to slide. This is what you build in front of any third-party API with a documented rate limit (GitHub: 5000/hr, OpenAI: 60/min, etc.).
Task Queue — Ordered, Bounded, Drainable
A semaphore is stateless about what runs next. A task queue gives you control over ordering, priority, and backpressure:
Priority queuing ensures high-importance work is never starved behind a pile of low-importance tasks. The stats call gives you live visibility into queue depth — useful for backpressure signalling.
Race Condition — The Classic Bug
A race condition occurs when two concurrent operations read and write shared state, and the outcome depends on which one wins the race:
The read-modify-write cycle must be atomic. The mutex serialises the critical section — only one withdrawal runs at a time through the danger zone, eliminating the race entirely.
Promise Pool — Controlled Parallel Iteration
Combining everything: process a large list with controlled concurrency, collecting all results, handling partial failures:
This pattern — fixed worker pool pulling from a shared index — is the correct foundation for bulk processing, import pipelines, web scrapers, and batch API calls. It is more efficient than slicing the array into chunks because workers self-balance: a slow item in one worker does not stall the others.
What You Have Learned
Concurrency is not just "use Promise.all". It is controlling how many things run, at what rate, in what order, and what happens when they conflict.
The key ideas:
awaitin sequence is serial — multiply your latency by the number of operations- Promise combinators:
all(all must succeed),allSettled(report every outcome),race(first wins),any(first success wins) - Semaphore: limit simultaneous operations — the correct tool for connection pools and API concurrency limits
- Rate limiter: limit throughput over time — N requests per window, regardless of completion speed
- Task queue: ordered, prioritised, backpressure-aware — the foundation of job processing systems
- Mutex: serialise the critical section of a read-modify-write cycle — eliminate race conditions
- Promise pool: fixed worker count pulling from a shared list — self-balancing, partial-failure-safe bulk processing
In the next lesson: Design Patterns in JavaScript — Singleton, Observer, Strategy, Command, and the patterns that appear constantly in real codebases. Not as academic taxonomy but as solutions to problems you will actually encounter.