UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

112 lines (83 loc) • 5.82 kB
--- 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 the run's total size**: Every batch still appends to the same [event log](/docs/how-it-works/event-sourcing#how-fast-a-log-grows), so a long enough list walks one run toward its [run limits](/worlds/vercel#per-run-limits) no matter how small the batches are. To shrink the run itself, bundle more items per step so one step covers many items, or spawn a [child workflow](/cookbook/advanced/child-workflows) per batch. Reach for one of those once a single run would grow past a few thousand events. - **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.