DEV Community

Alex Aslam
Alex Aslam

Posted on

Optimizing Async Operations: When Promise.all Destroys Your Performance (And When to Use Loops Instead)

The Async Performance Trap We Fell Into

Our database migration script was taking 8 hours to process 50,000 records. The code looked elegant:

await Promise.all(records.map(processRecord)); // "This should be fast!"
Enter fullscreen mode Exit fullscreen mode

But it crashed our database with 500 concurrent connections.

Here’s how we fixed itβ€”and when loops actually beat Promise.all.


1. The Dark Side of Promise.all

🚨 Problem #1: Uncontrolled Concurrency

// Spawns 50,000 promises at once!
await Promise.all(hugeArray.map(async (item) => {
  await db.insert(item); // πŸ’₯ Database melts down
}));
Enter fullscreen mode Exit fullscreen mode

βœ… Fix: Batch with p-limit

import pLimit from 'p-limit';
const limit = pLimit(10); // Max 10 concurrent ops

await Promise.all(
  hugeArray.map(item => limit(() => db.insert(item)))
);
Enter fullscreen mode Exit fullscreen mode

🚨 Problem #2: No Error Handling

// One failure rejects ALL promises
await Promise.all([
  fetchUser(1),
  fetchUser(2), // If this fails, entire batch dies
]);
Enter fullscreen mode Exit fullscreen mode

βœ… Fix: Use Promise.allSettled + filtering

const results = await Promise.allSettled(promises);
const successes = results.filter(r => r.status === 'fulfilled');
Enter fullscreen mode Exit fullscreen mode

🚨 Problem #3: Memory Overload

// Loads ALL records into memory before processing
const records = await getAllRecords(); // 500MB array
await Promise.all(records.map(processRecord));
Enter fullscreen mode Exit fullscreen mode

βœ… Fix: Stream with for-await-of

for await (const record of getRecordsStream()) {
  await processRecord(record); // Processes one at a time
}
Enter fullscreen mode Exit fullscreen mode

2. When Old-School Loops Win

Scenario Promise.all Loop
Order matters ❌ No βœ… Yes
Memory-constrained ❌ No βœ… Yes
Backpressure needed ❌ No βœ… Yes

Example: Sequential Processing

// Slower but safer
for (const item of items) {
  await process(item); // One at a time
}
Enter fullscreen mode Exit fullscreen mode

Example: Batched Loops

// Best of both worlds
const batchSize = 100;
for (let i = 0; i < items.length; i += batchSize) {
  const batch = items.slice(i, i + batchSize);
  await Promise.all(batch.map(process)); // Controlled concurrency
}
Enter fullscreen mode Exit fullscreen mode

3. Real-World Benchmarks

Method Time (10K ops) Memory Usage
Promise.all (unlimited) 2 min πŸ’₯ 1.2GB 🚨
p-limit (concurrency 10) 8 min 200MB
Batched loops (100/chunk) 6 min 50MB

Lesson learned: "Faster" isn’t always better.


Key Takeaways

⚑ Promise.all is great for β†’ Small, independent, non-IO-heavy tasks
🐒 Loops are better for β†’ Order-sensitive, memory-bound, or backpressured workflows
πŸ›  Hybrid approach β†’ Batched Promise.all inside loops

Which async pattern burned you? Share your war story!


Further Reading

Top comments (0)