---
title: Batching & Parallel Processing
description: Process large collections in parallel batches with failure isolation between groups.
type: guide
summary: Split items into fixed-size batches, process each batch concurrently with Promise.allSettled, and pace batches with sleep to avoid overloading downstream services.
---

<CopyPrompt
  text="Implement durable batch processing. Import `sleep` from `workflow`. In an exported &quot;use workflow&quot; function, split the input records into chunks of a fixed `batchSize`. For each batch, call a &quot;use step&quot; helper such as `processRecord(record)` for every record using `Promise.allSettled` so one record failure does not hide the rest. Record successes and failures in a serializable result object. Between batches, `await sleep(&quot;1s&quot;)` or another configured delay to respect downstream rate limits. Make the step idempotent using record IDs or external idempotency keys. Verify all-success, partial-failure, and rate-paced execution paths."
/>

Use batching when you need to process a large list of items in parallel while controlling concurrency. Items are split into fixed-size batches, each batch runs concurrently, and failures in one batch don't affect others.

## When to use this

- Bulk data imports (contacts, orders, or products from a comma-separated values (CSV) file)
- Processing hundreds or thousands of items against external APIs
- Calling rate-limited APIs where you need to control concurrency
- Any fan-out where you want failure isolation between groups
- High-concurrency fan-out, where one flat `Promise.all` over the whole list would put more work in flight than your downstream services or the run's event log should carry at once

## How it works

1. Records are split into fixed-size batches.
2. Each batch runs in parallel through `Promise.allSettled`, so failures in one record don't affect others.
3. A `sleep()` between batches paces requests to avoid overloading downstream services.
4. After all batches, a summary is returned with succeeded/failed counts.

## Pattern

The workflow splits records into chunks, processes each chunk concurrently, tracks results per batch, and returns a final tally.

```typescript
import { sleep } from "workflow";

type Record = { name: string; email: string; role: string };

declare function processRecord(record: Record): Promise<string>; // @setup

export async function batchImport(records: Record[], batchSize: number) {
  "use workflow";

  let totalSucceeded = 0;
  let totalFailed = 0;

  for (let i = 0; i < records.length; i += batchSize) {
    const batch = records.slice(i, i + batchSize);

    // Run batch in parallel: failures are isolated per record
    const outcomes = await Promise.allSettled( // [!code highlight]
      batch.map((record) => processRecord(record))
    );

    for (let j = 0; j < outcomes.length; j++) {
      if (outcomes[j].status === "fulfilled") {
        totalSucceeded++;
      } else {
        totalFailed++;
      }
    }

    // Pace between batches to avoid overloading downstream
    if (i + batchSize < records.length) {
      await sleep("1s"); // [!code highlight]
    }
  }

  return { total: records.length, succeeded: totalSucceeded, failed: totalFailed };
}
```

### Step function

Each record is processed in its own step with full Node.js access and automatic retries.

```typescript
type Record = { name: string; email: string; role: string };

async function processRecord(record: Record): Promise<string> {
  "use step";
  const res = await fetch(`https://api.example.com/contacts`, {
    method: "POST",
    body: JSON.stringify(record),
  });
  if (!res.ok) throw new Error(`Failed to import ${record.email}`);
  const { id } = await res.json();
  return id;
}
```

## Adapting to your use case

- Replace the `Record` type with your actual data shape, such as orders, images, or products.
- Replace `processRecord()` with your import logic, such as database upserts, API calls, or file processing.
- Tune `batchSize` and the `sleep()` duration to match your downstream rate limits.
- Add or remove tracking as needed; the pattern works with any item type.

## Tips

- **Use `Promise.allSettled` instead of `Promise.all`**: Use this pattern when you want to continue even if some items fail. `Promise.all` rejects on the first failure, while `allSettled` waits for everything and identifies failures.
- **Tune batch size to your downstream API limits**: If the API allows 10 concurrent requests, use `batchSize: 10`.
- **Batching bounds concurrency, not total event count**: Every record processed in its own step adds events to the same [event log](/docs/how-it-works/event-sourcing#how-fast-a-log-grows), regardless of batch size.
- **Consider using the QuickJS engine for long-running batches**: [Set `WORKFLOW_VM=quickjs`](/docs/configuration/runtime-tuning#workflow_vm) to use the experimental QuickJS runtime, which compresses and snapshots the event log and only loads events since the last snapshot on resume. This reduces replay overhead as the log grows and exempts eligible runs from the default [per-run event limit](/worlds/vercel#per-run-limits) ([`MAX_EVENTS_EXCEEDED`](/docs/errors/max-events-exceeded)). It does not reduce event count or speed up the work inside each step.
- **Choose the retry and run boundaries you need**: Keep one step per record when independent retries and visibility matter. To write fewer events, process multiple records within each step and make the grouped work idempotent, since a retry can repeat the whole group. Use a [child workflow](/cookbook/advanced/child-workflows) per batch when you need separate run histories and failure boundaries.
- **Add pacing with `sleep()`**: Add a delay between batches to respect rate limits. The sleep is durable and survives cold starts.
- **Treat each `processRecord` call as an independent step**: If one call fails, it retries up to three times without affecting other items in the batch.

## Key APIs

- [`"use workflow"`](/docs/foundations/workflows-and-steps): Marks the orchestrator function.
- [`"use step"`](/docs/foundations/workflows-and-steps): Marks functions that run with full Node.js access.
- [`sleep()`](/docs/api-reference/workflow/sleep): Adds a pacing delay between batches.
- [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled): Runs items in parallel and isolates failures.
