Skip to content
Docs

How to run background jobs in Next.js

Learn the durable way to run background jobs in Next.js on Vercel with the Workflow SDK, and when to reach for Queues or Cron Jobs.

Mitul ShahDesign Engineer

Copy link to headingIntroduction

A Next.js route handler exists to send a response fast. But real applications carry work that outlives that response, including sending a welcome email, processing an incoming webhook, calling a slow third-party API, retrying a failed charge, or running a task on a schedule. If you try to do that work inside the request, the user waits. If you start it without awaiting the result, nothing tracks it, retries it, or survives a crash or a new deployment. Background jobs in Next.js need somewhere durable to run, and the Workflow SDK is the place to achieve that.

Copy link to headingOverview

In this guide, you'll learn how to:

  • Move work out of a Next.js route handler and into a durable background job with the Workflow SDK and the "use workflow" directive
  • Add retries, delays, and scheduled runs without writing queue or state-management code
  • Decide between Vercel Workflows, Vercel Queues, and Cron Jobs for each kind of background work
  • Trigger, run, and observe background jobs locally and on Vercel

Copy link to headingThe fast path: a background job in a Next.js route

This is the shape of a background job in Next.js. Install the Workflow SDK:

npm i workflow

Installs the Workflow SDK. pnpm, yarn, and bun work too.

Wrap your Next.js config with withWorkflow() so the "use workflow" and "use step" directives compile:

next.config.ts
import { withWorkflow } from "workflow/next";
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// … rest of your Next.js config
};
export default withWorkflow(nextConfig);

Enables the Workflow directives across your Next.js project.

Write the background job as an ordinary async function with the "use workflow" directive at the top. Each unit of external work becomes a "use step" function, which retries on failure by default, up to the step’s retry limit:

workflows/user-signup.ts
import { sleep, FatalError } from "workflow";
export async function handleUserSignup(email: string) {
"use workflow";
const user = await createUser(email);
await sendWelcomeEmail(user);
await sleep("5s"); // Pause without consuming compute
await sendOnboardingEmail(user);
return { userId: user.id, status: "onboarded" };
}
async function createUser(email: string) {
"use step";
// Full Node.js access: database calls, APIs, and more
return { id: crypto.randomUUID(), email };
}
async function sendWelcomeEmail(user: { id: string; email: string }) {
"use step";
// Steps retry on unhandled errors by default
}
async function sendOnboardingEmail(user: { id: string; email: string }) {
"use step";
if (!user.email.includes("@")) {
throw new FatalError("Invalid Email"); // Throw FatalError to skip retries
}
}

Start the job from your route handler and respond right away. The workflow runs asynchronously and never blocks the response:

app/api/signup/route.ts
import { start } from "workflow/api";
import { handleUserSignup } from "@/workflows/user-signup";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { email } = await request.json();
// Runs in the background, doesn't block the response
await start(handleUserSignup, [email]);
return NextResponse.json({ message: "User signup workflow started" });
}

The route returns immediately while the background job runs to completion on its own.

You can trigger a workflow from a route handler, a Server Action, or any server-side code. Local development runs with npm run dev, and deploying to Vercel needs no extra configuration.

Copy link to headingHow it works

Vercel Workflows is a fully managed platform for building durable applications and AI agents in JavaScript, TypeScript, and Python. It builds on the open-source Workflow SDK. The "use workflow" and "use step" directives turn ordinary async functions into durable background jobs, recording each function's inputs and outputs so the platform can retry, pause, and resume without losing state.

Four abstractions cover the background-job patterns in this guide:

  • Workflow: A deterministic function that orchestrates steps over time. Its state lives in the event log rather than in memory. Each step’s input and output is recorded there, so the workflow resumes exactly where it left off after a pause, crash, or deployment allowing you to easily run long background jobs even across short serverless function executions.
  • Step: The unit that performs the work, with full Node.js access and built-in retries. Each result is persisted, so retries and recovery happen cleanly at step boundaries.
  • Sleep: pauses a workflow for anything from seconds to months without consuming compute resources.
  • Hook: lets a workflow wait for an external event such as a user action, a webhook, or a third-party API response, then resume.

On Vercel, Vercel Functions execute the workflow and step code, Vercel Queues enqueue and run those routes with at-least-once delivery, and managed persistence stores the encrypted state and event logs. Logs, metrics, and tracing appear in the Vercel dashboard with no configuration.

Copy link to headingMapping the background-job patterns

Background jobs in Next.js fall into a small set of recurring patterns. Here is where each one runs:

  • Retries: mark external calls with "use step".
  • Delayed jobs: call sleep() inside a workflow to pause for seconds, hours, days, or months. The pause consumes no compute.
  • Scheduled or recurring jobs: use Cron Jobs. A cron route can call start() to run a workflow on a schedule.
  • Waiting on webhooks or external events: use a hook to suspend the workflow until the event arrives, then resume with full state.

Copy link to headingScheduled background jobs with Cron Jobs

Cron Jobs handle time-based work such as backups, digest emails, or a nightly reconciliation task. Configure a schedule in vercel.json that points at a route in your app:

vercel.json
{
"crons": [
{
"path": "/api/hello",
"schedule": "0 5 * * *"
}
]
}

Runs a GET request to /api/hello every day at 05:00 UTC. The five fields are minute, hour, day of month, month, and day of week.

Vercel triggers the job with an HTTP GET request to the path on your production deployment. The request carries the user agent vercel-cron/1.0 and an x-vercel-cron-schedule header, but neither is a security boundary — any client can send them. The cron route is a public URL, so authenticate it: set a CRON_SECRET environment variable on your project, and Vercel will include it on every cron invocation as Authorization: Bearer <CRON_SECRET>. Have the route reject any request whose header doesn't match. See Securing cron jobs for details. Cron Jobs run only on production deployments. To do durable work on a schedule, keep the cron route thin and have it start() a workflow that handles the multi-step logic.

Copy link to headingWhat are Vercel Queues

Vercel Queues is a durable event streaming system for asynchronous workloads. You publish messages to a topic, and independent consumer groups process them with at-least-once delivery, automatic retries, sharding, and visibility timeouts. Queues provides lower-level message delivery controls. It is also the foundation the Workflow SDK builds on, offering a more complete developer experience on top of Queues.

Copy link to headingWhen to use Queues instead of Workflows

Workflows are the better fit for most of the use cases above: they build on Queues and handle retries, orchestration, and observability for you. Reach for Queues directly when:

  • Performance is critical. The Workflow SDK journals every step to provide durability and replay, which adds overhead. Consuming from a queue directly trades that away for raw throughput and lower latency.
  • You need fine-grained control over consumer groups, concurrency, or message-level delivery behavior that the Workflow abstraction doesn't expose.

A good default: start with Workflows. If a specific workload later proves performance-sensitive, eject that path to Queues—Workflows are built on Queues, so this narrows the abstraction rather than requiring a rearchitecture.

Copy link to headingSetting up a project with Vercel Queues

Publish a message from a route handler:

app/api/orders/route.ts
import { send } from '@vercel/queue';
export async function POST(request: Request) {
const order = await request.json();
const { messageId } = await send('orders', order);
return Response.json({ messageId });
}

Publishes an order to the orders topic and returns the message ID.

Process messages in a consumer route, and register it as a trigger in vercel.json:

app/api/queues/fulfill-order/route.ts
import { handleCallback } from '@vercel/queue';
export const POST = handleCallback(async (order, metadata) => {
// await doAnythingAsync(order);
});

Consumes each orders message. Adding the trigger below makes this route private, so it has no public URL and only Vercel's queue infrastructure can invoke it.

vercel.json
{
"functions": {
"app/api/queues/fulfill-order/route.ts": {
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "orders" }]
}
}
}

Binds the consumer route to the orders topic.

The dividing line is control level. If you need direct control over message publishing, consumption, and routing, use the Queues SDK. If you are building stateful, multi-step background jobs, start with Workflows.

Copy link to headingBest practices

Copy link to headingStart with Workflows

For stateful, multi-step background jobs in Next.js, use Workflows. You get retries, durable state, and observability without writing queue or state-management code. Reach for Queues only when you need low-level control over event delivery and fan-out.

Copy link to headingKeep steps small and focused

Put each external call in its own "use step" function. A step is the unit of retry and durability, so smaller steps give you tighter recovery and clearer observability.

Copy link to headingExclude the Workflow route from your proxy matcher

If your app has a proxy handler (proxy.ts, formerly middleware), exclude .well-known/workflow/* from the matcher. Otherwise the proxy intercepts Workflow's internal POST /.well-known/workflow/v1/flow request. The symptom is a [local world] Queue operation failed error or Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer.

Copy link to headingInspect runs before you ship

Run npx workflow web for a local dashboard, or npx workflow inspect runs from the terminal, to watch a background job step through its execution before you deploy.

Copy link to headingNext steps