Background Jobs and Durable Workflows with Netlify Async Workloads

How to run background jobs, event-driven tasks, and multi-step durable workflows on Netlify with Async Workloads, including retryable steps, delays, retries with backoff, and event chaining.

Tutorial

Some work should not happen inside a request. Sending a welcome email, processing an upload, calling a slow third-party API, running a nightly reconciliation: these need to run after you return a response, survive failures, and retry without repeating work already done. That usually means standing up a queue, a worker, and a state store.

Async Workloads gives you that on Netlify with no separate infrastructure. You write an event-triggered function, break it into retryable steps, and Netlify handles the queueing, retries, and durable state on top of Functions and Blobs. This guide covers the pieces you need: defining a workload, triggering it, splitting it into steps that survive retries, adding delays, and controlling retry behavior.

Prerequisites

  • A Netlify project on a credit-based plan, since workloads run on the same compute and storage credits as Functions and Blobs.
  • The Netlify CLI and Node 18 or later.

Install the package

npm install @netlify/async-workloads

Define a workload

A workload is a function in your functions directory that you wrap with asyncWorkloadFn. It runs in response to named events instead of an HTTP request, so it returns nothing to a caller:

import { asyncWorkloadFn, AsyncWorkloadEvent, AsyncWorkloadConfig } from '@netlify/async-workloads';
export default asyncWorkloadFn(async (event: AsyncWorkloadEvent) => {
console.log('processing', event.eventData);
});
export const asyncWorkloadConfig: AsyncWorkloadConfig = {
events: ['order-placed'],
};

The events array names the events that trigger this workload. One workload can listen for several events, and several workloads can listen for the same event.

Trigger a workload

Send an event from anywhere in your code, such as a Function that just finished handling a checkout:

import { AsyncWorkloadsClient } from '@netlify/async-workloads';
const client = new AsyncWorkloadsClient();
await client.send('order-placed', { orderId: 'A-4021' });

send returns as soon as Netlify accepts the event, so your request finishes immediately. The workload runs on its own.

Break the work into retryable steps

The reason to use Async Workloads rather than a plain background function is durable steps. Wrap each unit of work in event.step.run with a unique step ID. When a later step fails and the workload retries, every step that already succeeded returns its cached result instead of running again:

export default asyncWorkloadFn(async (event) => {
const order = await event.step.run('load-order', async () => {
return await loadOrder(event.eventData.orderId);
});
await event.step.run('charge-card', async () => {
return await chargeCard(order);
});
await event.step.run('send-receipt', async () => {
return await sendReceipt(order);
});
});

If send-receipt throws, the retry re-runs only send-receipt. It does not load the order again or charge the card twice. Step IDs must be unique within the workload, and that is what makes each step idempotent across retries.

Add delays

Use event.step.sleep to pause a workflow without holding a function open. Netlify schedules the resume, so a five-minute or five-day wait costs nothing while it waits:

await event.step.sleep('wait-before-reminder', '2 days');
await event.step.run('send-reminder', async () => {
return await sendReminder(order);
});

You can pass a duration string like "2 days" or a number of milliseconds.

Chain workloads with events

A step can emit its own event to hand off to another workload, which keeps each workload focused on one job:

await event.sendEvent('order-fulfilled', {
data: { orderId: order.id },
});

A separate workload listening for order-fulfilled picks it up. This is how you build a pipeline: several small workloads connected by events rather than one function that does everything.

Control retries

By default a failed workload retries with an escalating backoff (5s, then 20s, then 1m20s, then 8m, multiplying by four each time, up to a one-week ceiling). Override the count and schedule in the config:

export const asyncWorkloadConfig: AsyncWorkloadConfig = {
events: ['order-placed'],
maxRetries: 4, // 5 total attempts
backoffSchedule: (attempt) => (attempt < 2 ? '5 minutes' : '2 hours'),
};

When you already know a failure is permanent, or that a delay will help, throw a typed error instead of letting the default backoff run:

import { ErrorDoNotRetry, ErrorRetryAfterDelay } from '@netlify/async-workloads';
// A bad payload will never succeed: stop now.
throw new ErrorDoNotRetry('unknown order');
// A rate limit will clear: retry in 10 seconds.
throw new ErrorRetryAfterDelay({ message: 'rate limited', retryDelay: '10s' });

ErrorDoNotRetry ends the workload immediately. ErrorRetryAfterDelay overrides the backoff schedule for that one retry.

When to reach for it

Async Workloads fits background jobs (email, image processing, webhooks), event-driven fan-out (one event, several independent workloads), and multi-step workflows that must survive partial failure (a checkout that charges, fulfills, and notifies). It draws from your existing credit pool and needs no queue service, worker pool, or state store of your own.

For work that runs on a fixed clock rather than in response to an event, use Scheduled Functions instead. For a single fire-and-forget task with no steps or retries, a Background Function is simpler.

Resources