# External API calls inside loops use Promise.all batching

- **Pattern:** `ab-000244` (`ai-slop-cost-bombs.unrate-limited-spend.external-api-loops-batched`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000244
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000244)

## Why it matters

Sequential `await fetch(` inside a loop over a non-trivial collection is one of the most common AI-generated code patterns that degrades gracefully in testing and catastrophically in production. Each awaited call holds a thread-of-execution and an HTTP connection open until it resolves; a list of 200 items becomes 200 serialized round-trips, inflating p99 latency from 200ms to 40 seconds. The pattern also prevents proper error isolation — a single slow upstream response blocks everything behind it. On metered hosting (Vercel function duration, Lambda invocation time), sequential loops inflate compute costs linearly with list size.

## Severity rationale

High because sequential awaits in loops inflate response latency and compute billing linearly with input size, with no platform-level protection against runaway iteration.

## Remediation

Replace the sequential loop with `Promise.all` or `pMap` for bounded parallelism. In `src/lib/sync.ts` or whichever file contains the offending pattern:

```ts
// Bad: 200 items × 200ms = 40 seconds
for (const user of users) {
  await fetch(`/api/sync/${user.id}`)
}

// Good: 200 items at concurrency 10 ≈ 4 seconds
import pMap from 'p-map'
await pMap(
  users,
  async (user) => fetch(`/api/sync/${user.id}`, { signal: AbortSignal.timeout(5000) }),
  { concurrency: 10 },
)
```

Always pair with input length validation so the array itself is bounded before any parallelism runs.

## Detection

- **ID:** `external-api-loops-batched`
- **Severity:** `high`
- **What to look for:** Walk source files for `await fetch(`, `await axios.`, `await ky.`, `await got(` calls inside `for`/`while`/`forEach`/`.map(`/`.filter(`/`.reduce(` loops. For each match, count all loop-bound external API call sites and verify the call is inside `Promise.all([...])` or `Promise.allSettled([...])` OR is intentionally rate-paced. EXCLUDE calls with a `setTimeout` / `sleep` / `delay` between them.
- **Pass criteria:** 100% of loop-bound external API calls use `Promise.all`/`Promise.allSettled` OR have intentional pacing. Report: "X loop-bound API call sites, Y batched, 0 sequential."
- **Fail criteria:** At least 1 sequential `await fetch(` in a loop with no batching or pacing.
- **Skip (N/A) when:** No external API call patterns found.
- **Detail on fail:** `"1 sequential loop fetch: src/lib/sync.ts loops 'for (const u of users) { await fetch(\\`/api/sync/${u.id}\\`) }' — N+1 latency"`
- **Remediation:** Sequential awaits in loops are slow AND wasteful. Batch with bounded concurrency:

  ```ts
  // Bad: serial
  for (const user of users) {
    await fetch(`/api/sync/${user.id}`)
  }

  // Good: parallel with concurrency cap
  import pMap from 'p-map'
  await pMap(users, async (u) => fetch(`/api/sync/${u.id}`), { concurrency: 10 })
  ```

## External references

- iso-25010 performance-efficiency

Taxons: cost-efficiency, performance

HTML version: https://auditbuffet.com/patterns/ab-000244
