Home/Learn/JavaScript & TypeScript/Async Patterns — Concurrency, Queues and Cancellation

Async Patterns — Concurrency, Queues and Cancellation

Advanced
Async

Beyond a single request: limiting how many run at once, cancelling superseded work, and making retries safe.

Overview

Real applications rarely make one request. They fire many, and doing that naively either overwhelms the server or takes far too long. The three patterns worth having are a concurrency limit, so a thousand items do not become a thousand simultaneous requests; cancellation, so a superseded search does not overwrite a newer result; and idempotent retry, so repeating a failed request cannot double-charge someone. None of these are built in, which is why every codebase reimplements them.

Limiting Concurrency

Promise.all starts everything at once. With hundreds of items that is a self-inflicted denial of service.

A concurrency pool in a dozen lines
// All 500 at once — the server, or the browser's 6-connection limit, suffers
await Promise.all(ids.map(fetchOne))

// At most N in flight
async function pool(items, limit, worker) {
  const results = []
  const running = new Set()

  for (const [i, item] of items.entries()) {
    const p = Promise.resolve(worker(item, i))
      .then(r => { results[i] = r })
      .finally(() => running.delete(p))
    running.add(p)
    if (running.size >= limit) await Promise.race(running)
  }
  await Promise.all(running)
  return results
}

const pages = await pool(urls, 5, url => fetch(url).then(r => r.json()))

Cancelling Superseded Work

When a newer request supersedes an older one, the old response must not be allowed to win. There are two fixes.

Two fixes for the out-of-order response
// The race: request A (slow) then B (fast). B lands, then A lands
// and overwrites it with stale data.

// Fix 1 — abort the previous request
let controller
async function search(q) {
  controller?.abort()
  controller = new AbortController()
  const res = await fetch(`/api/search?q=${q}`, { signal: controller.signal })
  setResults(await res.json())
}

// Fix 2 — ignore stale responses by sequence number
let latest = 0
async function search(q) {
  const seq = ++latest
  const data = await fetchResults(q)
  if (seq === latest) setResults(data)     // drop if superseded
}

Idempotent Retry

Retrying a GET is safe. Retrying a POST can create two orders — unless the server can recognise the repeat.

Why POST retries need an idempotency key
// Safe to retry: GET, PUT, DELETE (idempotent by definition)
// Unsafe: POST — a retry after a timeout may create a second record,
// because you cannot tell "never arrived" from "arrived, reply lost"

// The fix is an idempotency key the server remembers:
const key = crypto.randomUUID()
await fetch('/api/orders', {
  method: 'POST',
  headers: { 'Idempotency-Key': key, 'Content-Type': 'application/json' },
  body: JSON.stringify(order),
})
// Retry with the SAME key — the server returns the original result
// instead of creating a second order. This is how payment APIs work.

// Backoff with jitter, so retries from many clients do not synchronise
const wait = Math.random() * 2 ** attempt * 200

Key Points to Remember

  • 1Promise.all starts every task at once — use a concurrency pool for large lists
  • 2A slow earlier response can overwrite a newer one; fix it with AbortController or a sequence check
  • 3GET, PUT and DELETE are safe to retry; POST is not, without an idempotency key
  • 4An idempotency key lets the server recognise a repeat and return the original result instead of acting twice
  • 5Add jitter to backoff so retries from many clients do not arrive together

Interview Questions

Sign in to ask Aria
1

How would you fetch 500 URLs without making 500 simultaneous requests?

Hard
2

A user types quickly and an older search response arrives after a newer one. How do you fix it?

Hard
3

Why is retrying a POST request dangerous, and what makes it safe?

Hard

Ask Aria about Async Patterns — Concurrency, Queues and Cancellation

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…