Ajie Logo Ajie.
Navigation

© 2026 Ajie Kusumadhany.

Back to Articles

Why Your Async Await Code Is Actually Blocking Everything Mengapa Kode Async Await Anda Justru Memblokir Semuanya

Ajie Ajie Kusumadhany
Jul 11, 2026 9 min read
Why Your Async Await Code Is Actually Blocking Everything Mengapa Kode Async Await Anda Justru Memblokir Semuanya

You refactored your callback hell into beautiful async/await syntax. Your code looks cleaner. It reads top-to-bottom like synchronous code.

And now your API responses are 3x slower than before.

Here's the uncomfortable truth: async/await doesn't magically make your code concurrent. It makes asynchronous code look synchronous, but most developers accidentally write it in a way that's actually synchronous.

Let me show you the exact mistake that's costing you hundreds of milliseconds on every request.

The Sequential Trap Everyone Falls Into

Look at this seemingly innocent code:

async function getUserData(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(userId);
  const comments = await fetchComments(userId);
  
  return { user, posts, comments };
}

Clean, readable, easy to follow. And painfully slow.

Each await keyword pauses execution until that promise resolves. Your code is literally waiting for fetchUser() to complete before even starting fetchPosts().

If each fetch takes 200ms, your total execution time is 600ms. But here's the kicker: these operations don't depend on each other. They could run in parallel.

What Async Await Actually Does

The async/await syntax is syntactic sugar over Promises. When you write await, you're telling JavaScript: "pause this function execution until this promise resolves."

That pause is the problem.

Behind the scenes, your function gets suspended at each await point. The event loop can do other work, yes, but your function is stuck waiting. The next line won't execute until the current promise completes.

This is exactly like synchronous blocking code, just with a nicer syntax.

The Promise.all Pattern You Should Be Using

Here's the fix that most developers never learn:

async function getUserData(userId) {
  const [user, posts, comments] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchComments(userId)
  ]);
  
  return { user, posts, comments };
}

Now all three fetches start simultaneously. Total execution time? Just 200ms—the time of the slowest operation.

You just made your code 3x faster with one line change.

When Sequential Actually Makes Sense

Not every async operation should run in parallel. Sometimes you need sequential execution:

async function processOrder(orderId) {
  const order = await fetchOrder(orderId);
  const validation = await validateInventory(order.items);
  const payment = await processPayment(order.total);
  
  return payment;
}

Each step depends on the previous one. This is correct sequential async code.

The rule: if operation B needs data from operation A, use sequential await. If they're independent, use Promise.all().

The Subtle Mistake in Loops

This one catches even experienced developers:

async function processUsers(userIds) {
  const results = [];
  
  for (const id of userIds) {
    const data = await fetchUserData(id);
    results.push(data);
  }
  
  return results;
}

Looks reasonable. Runs terribly.

With 10 users and 200ms per fetch, this takes 2 full seconds. It processes one user, waits, processes the next, waits again.

The parallel version:

async function processUsers(userIds) {
  const promises = userIds.map(id => fetchUserData(id));
  return await Promise.all(promises);
}

Same 10 users, same 200ms per fetch, but now it takes 200ms total. All fetches run concurrently.

Advanced Pattern: Promise.allSettled for Error Handling

Promise.all() has one fatal flaw: if any promise rejects, the entire operation fails immediately. Other promises are abandoned.

For operations where you want results even if some fail:

async function fetchMultipleAPIs(urls) {
  const results = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );
  
  return results.map((result, index) => ({
    url: urls[index],
    status: result.status,
    data: result.status === 'fulfilled' ? result.value : null,
    error: result.status === 'rejected' ? result.reason : null
  }));
}

Every fetch runs in parallel. Failures don't crash the entire operation. You get back all results with their success/failure status.

The Performance Matrix: Sequential vs Parallel

Pattern 3 Operations (200ms each) 10 Operations (200ms each) Best For
Sequential await 600ms 2000ms Dependent operations
Promise.all 200ms 200ms Independent operations, all-or-nothing
Promise.allSettled 200ms 200ms Independent operations, partial success OK
Promise.race 200ms 200ms First successful response wins

Controlling Concurrency: The Rate Limiting Pattern

Sometimes you need parallelism but with limits. Maybe your database can only handle 5 concurrent connections, or an API has rate limits.

Here's a production-ready pattern:

async function processWithLimit(items, limit, asyncFn) {
  const results = [];
  const executing = [];
  
  for (const item of items) {
    const promise = Promise.resolve().then(() => asyncFn(item));
    results.push(promise);
    
    if (limit <= items.length) {
      const executingPromise = promise.then(() =>
        executing.splice(executing.indexOf(executingPromise), 1)
      );
      executing.push(executingPromise);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// Usage: process 100 items with max 5 concurrent operations
await processWithLimit(userIds, 5, fetchUserData);

This maintains exactly 5 concurrent operations at any time. As soon as one completes, the next starts.

Real World Debugging: Finding Your Bottlenecks

How do you identify sequential bottlenecks in existing code?

Add timing logs:

async function getUserData(userId) {
  console.time('total');
  
  console.time('fetchUser');
  const user = await fetchUser(userId);
  console.timeEnd('fetchUser');
  
  console.time('fetchPosts');
  const posts = await fetchPosts(userId);
  console.timeEnd('fetchPosts');
  
  console.time('fetchComments');
  const comments = await fetchComments(userId);
  console.timeEnd('fetchComments');
  
  console.timeEnd('total');
  
  return { user, posts, comments };
}

If your total time equals the sum of individual operations, you have a sequential problem. It should be closer to the longest single operation.

The await in Conditional Trap

This subtle pattern causes sequential execution when you don't expect it:

async function smartFetch(useCache) {
  let data;
  
  if (useCache) {
    data = await fetchFromCache();
  } else {
    data = await fetchFromAPI();
  }
  
  const processed = await processData(data);
  return processed;
}

Looks fine. But what if you need both cache and API for comparison?

// Wrong: sequential
async function compareData() {
  const cached = await fetchFromCache();
  const fresh = await fetchFromAPI();
  return { cached, fresh };
}

// Right: parallel
async function compareData() {
  const [cached, fresh] = await Promise.all([
    fetchFromCache(),
    fetchFromAPI()
  ]);
  return { cached, fresh };
}

Memory Implications of Parallel Operations

Parallelism isn't free. Running 1000 database queries simultaneously will overwhelm your connection pool and likely crash your app.

Guidelines for safe parallelism:

  • HTTP requests: 10-50 concurrent requests is usually safe
  • Database queries: Match your connection pool size (usually 10-20)
  • File operations: Depends on I/O capacity, start with 5-10
  • CPU-intensive tasks: Match CPU core count

Use the rate limiting pattern above when you need to process large batches.

The Promise.any Pattern for Redundancy

When you have multiple sources for the same data and want the fastest response:

async function fetchFromFastestSource(userId) {
  try {
    const user = await Promise.any([
      fetchFromPrimaryDB(userId),
      fetchFromReplicaDB(userId),
      fetchFromCache(userId)
    ]);
    
    return user;
  } catch (errors) {
    // All sources failed
    throw new Error('All data sources unavailable');
  }
}

All three fetches start simultaneously. The first one to succeed wins. Others are ignored.

Pro Tips for Production Async Code

1. Add timeouts to prevent hanging:

function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Timeout')), ms)
    )
  ]);
}

const data = await withTimeout(fetchData(), 5000);

2. Use Promise.allSettled for batch operations:

You get visibility into every result, success or failure. Much easier to debug than catching errors from Promise.all.

3. Avoid awaiting in array methods:

// Wrong: sequential
const results = [];
items.forEach(async item => {
  const result = await process(item);
  results.push(result);
});

// Right: parallel
const results = await Promise.all(
  items.map(item => process(item))
);

4. Chain promises when possible:

// Less optimal
const user = await fetchUser(id);
const enriched = await enrichUserData(user);
return enriched;

// Better
return fetchUser(id)
  .then(enrichUserData);

5. Be explicit about error handling:

const results = await Promise.allSettled(operations);
const failures = results.filter(r => r.status === 'rejected');

if (failures.length > results.length / 2) {
  throw new Error('Too many failures');
}

Key Takeaways

Your async/await code is only as concurrent as you design it to be. The syntax doesn't make operations parallel—you have to explicitly choose parallelism.

Use sequential await when operations depend on each other. Use Promise.all() when they don't.

Watch out for loops. They're the most common place where developers accidentally create sequential bottlenecks.

Add timing logs to your critical paths. If the numbers don't add up, you've found your bottleneck.

The difference between sequential and parallel async code can be 10x performance improvement with zero infrastructure changes. Just better code organization.

Your users won't notice cleaner syntax. They will notice faster load times.

Anda merefaktor callback hell menjadi sintaks async/await yang indah. Kode Anda terlihat lebih bersih. Bisa dibaca dari atas ke bawah seperti kode synchronous.

Dan sekarang respons API Anda 3x lebih lambat dari sebelumnya.

Inilah fakta yang tidak nyaman: async/await tidak secara ajaib membuat kode Anda concurrent. Ini membuat kode asynchronous terlihat synchronous, tapi kebanyakan developer secara tidak sengaja menulisnya dengan cara yang benar-benar synchronous.

Saya akan tunjukkan kesalahan persis yang membuat Anda kehilangan ratusan milidetik di setiap request.

Jebakan Sequential yang Dialami Semua Orang

Lihat kode yang tampak innocent ini:

async function getUserData(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(userId);
  const comments = await fetchComments(userId);
  
  return { user, posts, comments };
}

Bersih, mudah dibaca, mudah diikuti. Dan menyakitkan lambatnya.

Setiap keyword await menghentikan eksekusi sampai promise tersebut selesai. Kode Anda benar-benar menunggu fetchUser() selesai sebelum bahkan memulai fetchPosts().

Jika setiap fetch memakan waktu 200ms, total waktu eksekusi Anda adalah 600ms. Tapi ini dia masalahnya: operasi-operasi ini tidak bergantung satu sama lain. Mereka bisa berjalan secara paralel.

Apa yang Sebenarnya Dilakukan Async Await

Sintaks async/await adalah syntactic sugar di atas Promise. Ketika Anda menulis await, Anda memberitahu JavaScript: "jeda eksekusi fungsi ini sampai promise ini selesai."

Jeda itulah masalahnya.

Di balik layar, fungsi Anda tersuspensi di setiap titik await. Event loop bisa mengerjakan hal lain, ya, tapi fungsi Anda terjebak menunggu. Baris berikutnya tidak akan dieksekusi sampai promise saat ini selesai.

Ini persis seperti kode blocking synchronous, hanya dengan sintaks yang lebih bagus.

Pattern Promise.all yang Seharusnya Anda Gunakan

Inilah perbaikan yang tidak pernah dipelajari kebanyakan developer:

async function getUserData(userId) {
  const [user, posts, comments] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchComments(userId)
  ]);
  
  return { user, posts, comments };
}

Sekarang ketiga fetch dimulai secara bersamaan. Total waktu eksekusi? Hanya 200ms—waktu operasi paling lambat.

Anda baru saja membuat kode Anda 3x lebih cepat dengan perubahan satu baris.

Kapan Sequential Benar-Benar Masuk Akal

Tidak setiap operasi async harus berjalan paralel. Kadang Anda perlu eksekusi sequential:

async function processOrder(orderId) {
  const order = await fetchOrder(orderId);
  const validation = await validateInventory(order.items);
  const payment = await processPayment(order.total);
  
  return payment;
}

Setiap langkah bergantung pada yang sebelumnya. Ini adalah kode async sequential yang benar.

Aturannya: jika operasi B membutuhkan data dari operasi A, gunakan await sequential. Jika mereka independen, gunakan Promise.all().

Kesalahan Halus dalam Loop

Yang ini bahkan menjebak developer berpengalaman:

async function processUsers(userIds) {
  const results = [];
  
  for (const id of userIds) {
    const data = await fetchUserData(id);
    results.push(data);
  }
  
  return results;
}

Terlihat wajar. Berjalan sangat buruk.

Dengan 10 user dan 200ms per fetch, ini memakan waktu 2 detik penuh. Ini memproses satu user, menunggu, memproses yang berikutnya, menunggu lagi.

Versi paralel:

async function processUsers(userIds) {
  const promises = userIds.map(id => fetchUserData(id));
  return await Promise.all(promises);
}

10 user yang sama, 200ms per fetch yang sama, tapi sekarang memakan waktu 200ms total. Semua fetch berjalan concurrent.

Pattern Advanced: Promise.allSettled untuk Error Handling

Promise.all() punya satu kelemahan fatal: jika ada promise yang reject, seluruh operasi langsung gagal. Promise lain ditinggalkan.

Untuk operasi di mana Anda ingin hasil meskipun ada yang gagal:

async function fetchMultipleAPIs(urls) {
  const results = await Promise.allSettled(
    urls.map(url => fetch(url).then(r => r.json()))
  );
  
  return results.map((result, index) => ({
    url: urls[index],
    status: result.status,
    data: result.status === 'fulfilled' ? result.value : null,
    error: result.status === 'rejected' ? result.reason : null
  }));
}

Setiap fetch berjalan paralel. Kegagalan tidak menghancurkan seluruh operasi. Anda mendapat kembali semua hasil dengan status sukses/gagal mereka.

Matriks Performa: Sequential vs Parallel

Pattern 3 Operasi (200ms each) 10 Operasi (200ms each) Terbaik Untuk
Sequential await 600ms 2000ms Operasi yang saling bergantung
Promise.all 200ms 200ms Operasi independen, semua atau tidak sama sekali
Promise.allSettled 200ms 200ms Operasi independen, sukses parsial OK
Promise.race 200ms 200ms Respons sukses pertama yang menang

Mengontrol Concurrency: Pattern Rate Limiting

Kadang Anda butuh paralelisme tapi dengan batasan. Mungkin database Anda hanya bisa menangani 5 koneksi concurrent, atau API punya rate limit.

Inilah pattern production-ready:

async function processWithLimit(items, limit, asyncFn) {
  const results = [];
  const executing = [];
  
  for (const item of items) {
    const promise = Promise.resolve().then(() => asyncFn(item));
    results.push(promise);
    
    if (limit <= items.length) {
      const executingPromise = promise.then(() =>
        executing.splice(executing.indexOf(executingPromise), 1)
      );
      executing.push(executingPromise);
      
      if (executing.length >= limit) {
        await Promise.race(executing);
      }
    }
  }
  
  return Promise.all(results);
}

// Penggunaan: proses 100 item dengan maks 5 operasi concurrent
await processWithLimit(userIds, 5, fetchUserData);

Ini mempertahankan tepat 5 operasi concurrent setiap saat. Begitu satu selesai, yang berikutnya dimulai.

Real World Debugging: Menemukan Bottleneck Anda

Bagaimana Anda mengidentifikasi bottleneck sequential di kode yang ada?

Tambahkan timing log:

async function getUserData(userId) {
  console.time('total');
  
  console.time('fetchUser');
  const user = await fetchUser(userId);
  console.timeEnd('fetchUser');
  
  console.time('fetchPosts');
  const posts = await fetchPosts(userId);
  console.timeEnd('fetchPosts');
  
  console.time('fetchComments');
  const comments = await fetchComments(userId);
  console.timeEnd('fetchComments');
  
  console.timeEnd('total');
  
  return { user, posts, comments };
}

Jika waktu total Anda sama dengan jumlah operasi individual, Anda punya masalah sequential. Seharusnya lebih dekat ke operasi tunggal terlama.

Jebakan await dalam Conditional

Pattern halus ini menyebabkan eksekusi sequential ketika Anda tidak mengharapkannya:

async function smartFetch(useCache) {
  let data;
  
  if (useCache) {
    data = await fetchFromCache();
  } else {
    data = await fetchFromAPI();
  }
  
  const processed = await processData(data);
  return processed;
}

Terlihat baik-baik saja. Tapi bagaimana jika Anda butuh cache dan API untuk perbandingan?

// Salah: sequential
async function compareData() {
  const cached = await fetchFromCache();
  const fresh = await fetchFromAPI();
  return { cached, fresh };
}

// Benar: parallel
async function compareData() {
  const [cached, fresh] = await Promise.all([
    fetchFromCache(),
    fetchFromAPI()
  ]);
  return { cached, fresh };
}

Implikasi Memory dari Operasi Parallel

Paralelisme tidak gratis. Menjalankan 1000 query database secara bersamaan akan membanjiri connection pool Anda dan kemungkinan crash aplikasi Anda.

Pedoman untuk paralelisme yang aman:

  • HTTP request: 10-50 concurrent request biasanya aman
  • Query database: Sesuaikan dengan ukuran connection pool Anda (biasanya 10-20)
  • Operasi file: Tergantung kapasitas I/O, mulai dengan 5-10
  • Task CPU-intensive: Sesuaikan jumlah core CPU

Gunakan pattern rate limiting di atas ketika Anda perlu memproses batch besar.

Pattern Promise.any untuk Redundansi

Ketika Anda punya beberapa sumber untuk data yang sama dan ingin respons tercepat:

async function fetchFromFastestSource(userId) {
  try {
    const user = await Promise.any([
      fetchFromPrimaryDB(userId),
      fetchFromReplicaDB(userId),
      fetchFromCache(userId)
    ]);
    
    return user;
  } catch (errors) {
    // Semua sumber gagal
    throw new Error('All data sources unavailable');
  }
}

Ketiga fetch dimulai secara bersamaan. Yang pertama berhasil menang. Yang lain diabaikan.

Tips Praktis untuk Kode Async Production

1. Tambahkan timeout untuk mencegah hanging:

function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Timeout')), ms)
    )
  ]);
}

const data = await withTimeout(fetchData(), 5000);

2. Gunakan Promise.allSettled untuk operasi batch:

Anda mendapat visibilitas ke setiap hasil, sukses atau gagal. Jauh lebih mudah di-debug daripada menangkap error dari Promise.all.

3. Hindari awaiting di array method:

// Salah: sequential
const results = [];
items.forEach(async item => {
  const result = await process(item);
  results.push(result);
});

// Benar: parallel
const results = await Promise.all(
  items.map(item => process(item))
);

4. Chain promise bila memungkinkan:

// Kurang optimal
const user = await fetchUser(id);
const enriched = await enrichUserData(user);
return enriched;

// Lebih baik
return fetchUser(id)
  .then(enrichUserData);

5. Eksplisit tentang error handling:

const results = await Promise.allSettled(operations);
const failures = results.filter(r => r.status === 'rejected');

if (failures.length > results.length / 2) {
  throw new Error('Too many failures');
}

Kesimpulan Utama

Kode async/await Anda hanya se-concurrent yang Anda desain. Sintaksnya tidak membuat operasi paralel—Anda harus secara eksplisit memilih paralelisme.

Gunakan await sequential ketika operasi bergantung satu sama lain. Gunakan Promise.all() ketika tidak.

Waspadai loop. Mereka adalah tempat paling umum di mana developer secara tidak sengaja membuat bottleneck sequential.

Tambahkan timing log ke critical path Anda. Jika angkanya tidak cocok, Anda telah menemukan bottleneck Anda.

Perbedaan antara kode async sequential dan parallel bisa menjadi peningkatan performa 10x tanpa perubahan infrastruktur sama sekali. Hanya organisasi kode yang lebih baik.

User Anda tidak akan memperhatikan sintaks yang lebih bersih. Mereka akan memperhatikan waktu loading yang lebih cepat.

#JavaScript #Async #Performance #Node.js #Programming