---
title: How to run background jobs in Next.js
description: 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.
url: /kb/guide/how-to-run-background-jobs-in-nextjs-on-vercel
canonical_url: "https://vercel.com/kb/guide/how-to-run-background-jobs-in-nextjs-on-vercel"
published: 2026-08-24
last_updated: 2026-08-25
authors: Mitul Shah
related:
  - /docs/functions
  - /docs/queues
  - /docs/cron-jobs
  - /docs/cron-jobs/manage-cron-jobs
  - /docs/workflows
  - /docs/workflows/concepts
  - /docs/queues/concepts
  - /docs/cron-jobs/quickstart
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

## Introduction

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](https://workflow-sdk.dev/) is the place to achieve that.

## Overview

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
  

## The 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:

```bash
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:

```tsx
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:

```tsx
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:

```tsx
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.

## How 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](https://vercel.com/docs/functions) execute the workflow and step code, [Vercel Queues](https://vercel.com/docs/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.

## Mapping 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](https://vercel.com/docs/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.
  

## Scheduled 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:

```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](https://vercel.com/docs/cron-jobs/manage-cron-jobs#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.

## What 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.

### When 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.

### Setting up a project with Vercel Queues

Publish a message from a route handler:

```tsx
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`:

```tsx
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._

```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.

## Best practices

### Start 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.

### Keep 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.

### Exclude 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`. ### Inspect 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. ## Next steps - [Vercel Workflows](https://vercel.com/docs/workflows) and [Workflows concepts](https://vercel.com/docs/workflows/concepts)
  
- [Vercel Queues](https://vercel.com/docs/queues) and the [Queues vs Workflows comparison](https://vercel.com/docs/queues/concepts)
  
- [Cron Jobs](https://vercel.com/docs/cron-jobs) and the [Cron Jobs quickstart](https://vercel.com/docs/cron-jobs/quickstart)
  
- [Vercel Functions](https://vercel.com/docs/functions)
  
- [Next.js Route Handlers](https://nextjs.org/docs/app/api-reference/file-conventions/route)