Running 1000 fetch calls in parallel can overwhelm your server or hit rate limits. This article covers three patterns: sequential execution, pooled concurrency (N at a time), and retry with exponential backoff.
Prerequisites: Async #3 — Promises
1. Execute Async Tasks in Sequence
Run an array of async functions one after another — each waits for the previous to complete:
Loading editor...
Promise.all would run them concurrently (~300ms) but with no concurrency control.
2. Concurrency Pool — Run N at a Time
Run tasks with a maximum concurrency limit:
Loading editor...
3. Simpler Pool Using Promise.race
An alternative implementation — start limit tasks, then replace each as it completes:
Loading editor...
4. Retry with Exponential Backoff
When an operation can fail transiently (network, rate limits), retry with increasing delays:
Loading editor...
5. Retry + Concurrency Pool Combined
Real-world scenario: process a list of URLs with concurrency control AND per-item retry:
Loading editor...
6. Timeout Wrapping
Add a timeout to any async operation — reject if it takes too long:
Loading editor...
Key Takeaways
- Sequential:
for...of+await— simplest, preserves order, no concurrency overhead. - Pool (N at a time): worker pattern — good when you need to limit load on server/DB.
- Promise.race approach: start tasks, replace as they complete — elegant alternative to worker pattern.
- Retry with backoff: exponential delay + jitter prevents thundering herd on recovery.
- Timeout wrapping:
Promise.race(promise, timeoutPromise)— reject if the operation exceeds the limit.
Next: Async #9 — AbortController & Cancelable Async — cancel fetch requests, remove listeners, and abort async work.
