---
title: Running background workers on Vercel
description: Run background jobs on Vercel with waitUntil, Cron Jobs, Queues, Workflows, Daemons, or Sandbox. Compare the options and pick the right one for your workload.
url: "https://vercel.com/kb/guide/background-workers-on-vercel"
published: 2026-11-02
last_updated: 2026-11-02
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

## Can you run background workers on Vercel?

Yes, Vercel runs background work. Use `waitUntil` for short tasks that can finish after the response, Cron Jobs for schedules, Vercel Queues for event-driven jobs with retries, and Vercel Workflows for multi-step jobs with durable waits.

Vercel Daemons keep workers running so they can pull new jobs. Use them for Celery processes and queue consumers that run `ffmpeg`.

## Which background work option should you use on Vercel?

Choose a background work option by how long the job runs and what starts it.

| Option                           | Best for                                                                             | How long it can run                                                       | What starts the work                                         |
| -------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `waitUntil` in a Vercel Function | Small follow-up tasks after a response, such as logging or cache warming             | The function's max duration                                               | The request that triggered the function                      |
| Cron Jobs                        | Scheduled tasks, such as nightly cleanup, reports, and syncs                         | The function's max duration                                               | The schedule in `vercel.json`                                |
| Vercel Queues, push mode         | Event-driven jobs that need retries and a concurrency limit                          | The consumer function's max duration                                      | Vercel invoking a private consumer function for each message |
| Vercel Queues, poll mode         | Consumers that need full control, or that run outside Vercel                         | As long as the consumer runs                                              | The consumer pulling messages                                |
| Vercel Workflows                 | Multi-step jobs that wait, retry, and resume, such as onboarding or order processing | Minutes to months                                                         | Your app starting the workflow, followed by durable steps    |
| Vercel Daemons                   | Always-on workers, such as Celery, Sidekiq, BullMQ, `ffmpeg`, and CI runners         | Indefinitely                                                              | The daemon pulling from a queue, job database, or API        |
| Vercel Sandbox                   | Running untrusted or AI-generated code in isolation                                  | One session, up to 24 hours on Pro and Enterprise and 45 minutes on Hobby | An API call to the Sandbox SDK                               |

With Fluid compute enabled, Vercel Functions have a default max duration of 300 seconds on every plan. Pro and Enterprise teams can raise it to 800 seconds, or to 1,800 seconds (30 minutes) in beta for supported Node.js, Bun, and Python runtimes ([duration docs](https://vercel.com/docs/functions/configuring-functions/duration)). Sandbox has separate [session duration limits](https://vercel.com/docs/sandbox/pricing#runtime-limits).

Vercel Workflows has been generally available since April 2026. Vercel Queues and Vercel Daemons are in public beta. During the beta, Vercel Daemons run only in production, don't autoscale, and can't receive inbound traffic.

Compare [Functions, containers, and Daemons](https://vercel.com/kb/guide/vercel-functions-containers-daemons) to choose a compute type. For job delivery and durable orchestration, weigh [Queues against Workflows](https://vercel.com/docs/queues/concepts#vercel-queues-vs-workflows).

## How do you run a task after the response on Vercel?

Use `waitUntil` from `@vercel/functions` to let an asynchronous task continue after a Vercel Function sends its response. The task starts when you create the promise, and `waitUntil` keeps the invocation alive within its existing max duration.

In Next.js 15.1 and later, use `after()` from `next/server` to schedule a callback that starts after the response.

### Example: send a welcome email after signup

This signup route creates the account and starts the welcome email without waiting for it to finish before returning the response:

```typescript
import { waitUntil } from '@vercel/functions';
import { createUser, sendWelcomeEmail } from '@/lib/users';

export async function POST(request: Request) {
  const { email } = await request.json();
  const user = await createUser(email);

  // Let the email task continue within this invocation's max duration.
  waitUntil(sendWelcomeEmail(user));

  return Response.json({ id: user.id });
}
```

In the App Router, `after()` runs the email callback after the response:

```typescript
import { after } from 'next/server';
import { createUser, sendWelcomeEmail } from '@/lib/users';

export async function POST(request: Request) {
  const { email } = await request.json();
  const user = await createUser(email);

  after(() => sendWelcomeEmail(user));

  return Response.json({ id: user.id });
}
```

### When does `waitUntil` stop being enough?

`waitUntil` provides no retries, queue, or task record. If the email provider fails or the function hits its max duration, the task is lost. That trade-off suits logging, analytics, and cache warming that tolerate occasional task loss.

Use Vercel Queues for tasks that need retries, or Vercel Workflows for jobs with several steps or waits. See the [`waitUntil`](https://vercel.com/docs/functions) [API](https://vercel.com/docs/functions) for details.

## How do you run scheduled jobs on Vercel?

Use Cron Jobs to run a Vercel Function on a schedule defined in `vercel.json`. Vercel sends an HTTP GET request to the configured path on your production deployment. Each run must finish within the function's max duration.

### Example: sync data from a CRM every night

This schedules `/api/sync-crm` daily at 05:00 UTC. Hobby may invoke it within that hour; Pro and Enterprise schedule to the minute:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "crons": [
    {
      "path": "/api/sync-crm",
      "schedule": "0 5 * * *"
    }
  ]
}
```

Set a random `CRON_SECRET` of at least 16 characters in your production environment. Vercel sends cron requests with `Authorization: Bearer <CRON_SECRET>`.

Reject requests if the secret is missing or the header doesn't match.

Save this public Next.js App Router handler as `app/api/sync-crm/route.ts`:

```typescript
import { syncContactsFromCrm } from '@/lib/crm';

export const maxDuration = 300;

export async function GET(request: Request) {
  const secret = process.env.CRON_SECRET;
  const authorization = request.headers.get('authorization');

  if (!secret || authorization !== `Bearer ${secret}`) {
    return new Response('Unauthorized', { status: 401 });
  }

  const synced = await syncContactsFromCrm();
  return Response.json({ synced });
}
```

Cron Jobs run only in production. See [Securing cron jobs](https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs) for `CRON_SECRET` setup.

Cron delivery can be missed or duplicated, and Vercel does not retry a failed invocation. Make the sync safe to repeat and reconcile changes since its last successful run. Hobby supports one run per day for each cron job; Pro and Enterprise support intervals as short as one minute.

### When should a scheduled job use Vercel Workflows?

Scheduled jobs that need durable retries, have several steps, or may exceed the function's max duration should start a workflow from the cron route.

Call `start()` from the Workflow SDK and leave retries and state to the workflow. Follow the [Next.js background jobs pattern](https://vercel.com/kb/guide/how-to-run-background-jobs-in-nextjs-on-vercel) for this setup.

Starting a workflow does not recover a cron invocation that never reached the route. Use the next scheduled run to reconcile missed work.

## How do you process jobs from a queue on Vercel?

Use Vercel Queues in push mode to process jobs as events arrive. Your app publishes a message to a topic, and Vercel invokes a private consumer function for each message, retrying failed deliveries until the message expires or reaches a configured delivery cap. The consumer's max duration applies to each delivery. Plan how to handle [messages that keep failing](https://vercel.com/i/dead-letter-queue-guide) once retries stop.

Vercel Queues delivers each message at least once, so consumers must be safe to run twice. Design consumers for [idempotency](https://vercel.com/i/what-is-idempotency), so a repeated delivery has the same effect as the first. Consumer functions have no public URL, which means only Vercel's queue infrastructure can invoke them.

### Example: resize images when users upload them

Image processing can fail on a bad file or a slow storage call. Vercel Queues buffers jobs from upload bursts and retries failures until message retention expires.

Save this subscriber as `queues/uploads.py`. It limits the consumer group to 10 concurrent deliveries and calls an application-owned `app_clients.images` helper:

```python
from pydantic import BaseModel
from vercel.queue import Topic, subscribe

from app_clients import images


class Upload(BaseModel):
    upload_id: str
    blob_url: str


uploads = Topic[Upload]("image-uploads")


@subscribe(topic=uploads, consumer_group="thumbnails", max_concurrency=10)
async def make_thumbnail(upload: Upload) -> None:
    # This application helper must publish the completed image idempotently.
    await images.resize_and_store(
        source=upload.blob_url,
        destination=f"thumbnails/{upload.upload_id}.webp",
        width=400,
    )
```

Implement `resize_and_store` for immutable source uploads and atomic publication of a completed thumbnail. For example, conditionally create the destination object and treat an existing thumbnail for the same upload and resize settings as success. The destination path alone does not make concurrent retries safe.

Register the module in `pyproject.toml` to build a private queue consumer:

```toml
[[tool.vercel.subscribers]]
entrypoint = "queues.uploads"
```

Call this helper from your upload route to publish the message. The same upload ID produces the same idempotency key, so repeated publishes are deduplicated within the message's retention window, up to 24 hours:

```python
from vercel.queue import send

from queues.uploads import Upload, uploads


async def queue_thumbnail(upload: Upload) -> None:
    await send(uploads, upload, idempotency_key=f"thumb-{upload.upload_id}")
```

### Which settings control retries and load?

| Setting                              | What it controls                                                                | Where you set it                                                        |
| ------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `maxDeliveries`                      | Delivery attempt cap. No cap by default; message retention still limits retries | The `queue/v2beta` trigger in `vercel.json` (JavaScript and TypeScript) |
| `max_attempts`                       | The same delivery cap for Python subscribers                                    | `@subscribe(...)`                                                       |
| `retryAfterSeconds`                  | Delay before a failed message is retried. 60 seconds by default                 | The `queue/v2beta` trigger in `vercel.json`                             |
| `max_concurrency`                    | How many deliveries run at once for one consumer group                          | `@subscribe(...)` (Python only)                                         |
| `idempotency_key` / `idempotencyKey` | Deduplicates repeated publishes for up to 24 hours                              | `send(...)`                                                             |

JavaScript and TypeScript consumers declare their triggers in `vercel.json`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "functions": {
    "app/api/queues/make-thumbnail/route.ts": {
      "experimentalTriggers": [
        {
          "type": "queue/v2beta",
          "topic": "image-uploads",
          "retryAfterSeconds": 60,
          "maxDeliveries": 10
        }
      ]
    }
  }
}
```

The JavaScript SDK and the `queue/v2beta` trigger don't document a max concurrency option yet. To cap concurrency from JavaScript, consume the topic in poll mode from a Vercel Daemon, where the number of instances and loops sets how many messages run at once.

[Set up a Python queue consumer](https://vercel.com/kb/guide/python-and-vercel-queues) with FastAPI, retries, and tests.

### When should you use poll mode?

Use [poll mode](https://vercel.com/docs/queues/poll-mode) when the consumer needs to decide when to pull work, or when it runs outside Vercel. Poll-mode consumers often run as Vercel Daemons, which keep pulling messages without a per-message function invocation.

## How do you run multi-step or long-running jobs on Vercel?

Use Vercel Workflows for jobs with several steps, waits, or external events. Workflows are async functions marked with `"use workflow"`, and each unit of external work is a `"use step"` function. Vercel records every step's result, so a workflow can retry a failed step, sleep for minutes to months without using compute, and resume after a crash or a new deployment.

Workflows run on Vercel Functions and Vercel Queues, but no single function needs to run for the whole job. Each step runs in its own invocation and must finish within the function's max duration.

### Example: fulfill an order and request a review three days later

This workflow charges an order, reserves stock, creates a shipment, and sends a shipping email. After a three-day wait, it requests a review. Each step can run again after a failure, so its external calls must be safe to repeat.

The example assumes application-owned clients exported from `@/lib/order-integrations`. Each client must enforce the operation key through its provider's idempotency API or a durable record coordinated with the side effect. An option named `idempotencyKey` does not enforce this contract.

```typescript
import { sleep, FatalError } from 'workflow';
import { payments, inventory, warehouse, email } from '@/lib/order-integrations';

export async function fulfillOrder(orderId: string) {
  'use workflow';

  const charge = await chargeCard(orderId);
  await reserveStock(orderId);
  const shipment = await createShipment(orderId);
  await sendShippingEmail(orderId, shipment.trackingUrl);

  await sleep('3d');
  await sendReviewRequest(orderId);

  return { orderId, chargeId: charge.id };
}

async function chargeCard(orderId: string) {
  'use step';
  const result = await payments.charge(orderId, {
    idempotencyKey: `charge-${orderId}`,
  });
  if (result.declined) {
    throw new FatalError('Card declined');
  }
  return result;
}

async function reserveStock(orderId: string) {
  'use step';
  await inventory.reserve(orderId, {
    idempotencyKey: `reserve-${orderId}`,
  });
}

async function createShipment(orderId: string) {
  'use step';
  return warehouse.createShipment(orderId, {
    idempotencyKey: `ship-${orderId}`,
  });
}

async function sendShippingEmail(orderId: string, trackingUrl: string) {
  'use step';
  await email.send({
    template: 'shipped',
    orderId,
    trackingUrl,
    idempotencyKey: `shipping-email-${orderId}`,
  });
}

async function sendReviewRequest(orderId: string) {
  'use step';
  await email.send({
    template: 'review-request',
    orderId,
    idempotencyKey: `review-email-${orderId}`,
  });
}
```

Start the workflow from a route handler and respond right away:

```typescript
import { start } from 'workflow/api';
import { fulfillOrder } from '@/workflows/fulfill-order';

export async function POST(request: Request) {
  const { orderId } = await request.json();
  await start(fulfillOrder, [orderId]);
  return Response.json({ status: 'processing' });
}
```

Unhandled step errors retry up to three times by default; `FatalError` stops retries. Keep the same operation key across attempts and retain records of completed business operations for as long as those operations can be replayed.

The application must deduplicate order submissions because repeated requests can start new workflow runs. See [step and run deduplication](https://workflow-sdk.dev/docs/foundations/idempotency).

### Why use Workflows instead of chaining queues?

You can build the same order flow by publishing a message at the end of each consumer, but you then own the state between steps, including which steps succeeded before a failure.

The three-day wait needs a delayed message with enough retention left for processing and retries. Queues retains messages for 24 hours by default; both retention and delivery delay can be extended to 7 days.

Vercel Workflows keeps that state in an event log. If the shipment fails, Vercel Workflows retries only `createShipment`, not the charge, and the dashboard shows every run and step. Use Vercel Queues directly when you need raw throughput or fine-grained control over consumer groups, and use Vercel Workflows for business processes with steps and waits. For a fuller comparison, see [when to use queues vs. workflows](https://vercel.com/i/queues-vs-workflows).

For setup, retries, and hooks that wait for webhooks, see the [Next.js workflow walkthrough](https://vercel.com/kb/guide/how-to-run-background-jobs-in-nextjs-on-vercel) and [workflow concepts](https://vercel.com/docs/workflows/concepts). Vercel Workflows also supports [Python](https://vercel.com/docs/workflows/python).

## How do you run an always-on worker on Vercel?

Use Vercel Daemons for containerized workers that stay running and pull work. Declare each daemon in `vercel.json` with a `Dockerfile`. Vercel builds the image, starts the configured instances in each region, and restarts any instance that exits. Daemons run on Fluid compute, the same platform as Vercel Functions.

Vercel Daemons don't receive traffic. Each instance pulls from a queue, job database, workflow engine, or external API. This suits workers built with Celery, Sidekiq, BullMQ, or Temporal.

Still comparing Daemons with container services or a VPS? See [where to host background workers](https://vercel.com/i/where-to-host-background-workers).

During the public beta, Vercel Daemons have these constraints:

- Daemons run only in production. Preview deployments don't start them.
  
- Daemons don't autoscale. Set 0 to 100 instances per region in `vercel.json` or the dashboard, within the plan and project quotas listed below.
  
- Each instance runs on Linux `amd64`. Hobby supports 1 or 2 GiB with 1 vCPU. Pro and Enterprise also support 4 GiB with 2 vCPUs and 8 GiB with 4 vCPUs. The default is 2 GiB.
  
- For rollouts, scale-downs, and removal, Vercel sends `SIGTERM` and waits 30 seconds by default. Set `terminationGracePeriodSeconds` to allow up to 3,600 seconds.
  
- The project's root directory must be the repository root.
  

### Example: transcode uploaded videos with `ffmpeg`

Use a daemon when the video processor continuously polls for uploads or handles transcodes that exceed the function's max duration. The app publishes one job to Vercel Queues per upload, and a Python worker runs `ffmpeg` for each job.

The processor's `Dockerfile` installs `ffmpeg` alongside the Python dependencies:

```dockerfile
FROM python:3.12-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends ffmpeg \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY processor.py .
CMD ["python", "processor.py"]
```

Declare the daemon next to your app in `vercel.json`. On Pro or Enterprise, this configuration runs two instances in `iad1`, each with 4 GiB of memory and 2 vCPUs. It allows up to 10 minutes to finish a transcode after `SIGTERM`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "daemons": {
    "videos": {
      "root": "processor/",
      "entrypoint": "Dockerfile",
      "instances": { "iad1": 2 },
      "resources": { "requests": { "memory": "4Gi" } },
      "terminationGracePeriodSeconds": 600
    }
  }
}
```

The processor loop should poll the queue, stop accepting new jobs when it receives `SIGTERM`, and acknowledge each message only after the transcoded file is stored. If an instance stops mid-job, the unacknowledged message becomes visible again after its visibility timeout, and another instance picks it up. Use the [Python polling worker](https://vercel.com/kb/guide/python-and-vercel-queues#run-a-polling-worker-on-vercel-daemons) for a complete implementation.

### Example: move a Celery worker from Heroku

Heroku apps often start Celery from a `Procfile` entry such as `worker: celery -A app worker --loglevel=info`. Run that command as a Vercel Daemon using your existing backend image and broker:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "daemons": {
    "celery": {
      "root": "backend/",
      "entrypoint": "Dockerfile",
      "command": ["celery", "-A", "app", "worker", "--loglevel=info"],
      "instances": { "iad1": 1 }
    }
  }
}
```

The worker can keep its task code and broker. Set its broker URL through a production environment variable such as `CELERY_BROKER_URL`.

Celery acknowledges tasks before execution by default. For idempotent tasks that must return to the queue after an interruption, configure late acknowledgment with `task_acks_late`. Review `task_reject_on_worker_lost` if tasks should also be requeued after the executing child process exits abruptly.

Set `terminationGracePeriodSeconds` long enough for normal tasks to finish, then test forced interruption as well as graceful shutdown.

Verify recovery behavior before moving production work:

1. Deploy the daemon with the same broker and task definitions as the Heroku worker.
   
2. Confirm that the daemon processes jobs. Test that graceful shutdown finishes a job and that forced termination recovers a job under your chosen acknowledgment policy.
   
3. Scale the Heroku worker dyno to zero, then remove it.
   

Vercel runs `command` directly, so environment variables in the array aren't expanded. To use `WORKER_CONCURRENCY`, replace the daemon's `command` value with:

```json
[
  "sh",
  "-c",
  "exec celery -A app worker --concurrency \"$WORKER_CONCURRENCY\""
]
```

Moving a worker doesn't move the jobs stored in its broker. If the broker is also changing, for example from Heroku Data for Redis to a Marketplace provider, migrate the broker first and account for delayed jobs and scheduled retries. Moving workers from Render instead? Follow the steps to [migrate Render workers to Vercel Daemons](https://vercel.com/kb/guide/migrate-render-workers-to-vercel-daemons).

Learn to [configure, scale, and monitor daemons](https://vercel.com/kb/guide/vercel-daemons) and test them locally with Docker.

## How do you run AI agents in the background on Vercel?

Use Vercel Workflows to coordinate steps around an agent call, such as generating a report, waiting for approval, and publishing it. Vercel Queues in push mode delivers agent jobs to functions when you don't need to control polling.

Choose Vercel Daemons for workers that need to control the pull rate, hold connections open, or run tools that need system binaries. Keep durable sessions in the agent framework and use the daemon to feed them work or perform CPU-heavy tasks while a session waits.

Vercel Sandbox isolates agent-generated code from your app's credentials.

### How do you run eve agents with Vercel Daemons?

Within the eve agent's Vercel project, a daemon takes a queued job and starts a session through `POST /eve/v1/session`. It reads the session stream and acknowledges the job after the turn completes or its failure is saved for review.

An eve tool can also hand heavy work to a daemon through Vercel Queues. The session waits without holding compute until the daemon posts the result.

Keep `vercelOidc()` in the agent's eve channel auth list so it accepts the same project's daemon identity. The daemon reads its rotating token file for each request. Use `operationId` for session creation and `idempotencyKey` for follow-up messages.

This excerpt uses the [shared worker dependencies and helpers](https://vercel.com/kb/guide/vercel-daemons-eve-agents#how-do-you-set-up-a-shared-worker-image). It keys `operationId` on the queue message ID so redelivery can reconnect to the existing session while that session remains resumable.

```typescript
import { PollingQueueClient } from '@vercel/queue';
import { startSession, waitForFirstTurn } from './lib/eve.ts';
import { idle, shutdown } from './lib/shutdown.ts';

interface Ticket {
  id: string;
  subject: string;
  body: string;
}

const queue = new PollingQueueClient({
  region: 'iad1',
  deploymentId: null,
});

async function handleTicket(
  ticket: Ticket,
  metadata: { messageId: string },
): Promise<void> {
  shutdown.signal.throwIfAborted();
  const sessionId = await startSession(
    [
      `Triage support ticket ${ticket.id}.`,
      `Subject: ${ticket.subject}`,
      '',
      ticket.body,
    ].join('\n'),
    `ticket-${metadata.messageId}`,
  );
  const turn = await waitForFirstTurn(sessionId, shutdown.signal);
  if (turn.outcome !== 'completed') {
    await queue.send(
      'support-tickets-failures',
      {
        ticket,
        messageId: metadata.messageId,
        sessionId,
        outcome: turn.outcome,
      },
      {
        idempotencyKey: `triage-failed-${metadata.messageId}`,
        retentionSeconds: 604800,
      },
    );
    console.error(`Triage ${turn.outcome} for ticket ${ticket.id}`);
  }
}

while (!shutdown.signal.aborted) {
  try {
    const result = await queue.receive<Ticket>(
      'support-tickets',
      'triage-agent',
      handleTicket,
    );
    if (!result.ok) await idle(2000);
  } catch (error) {
    if (!shutdown.signal.aborted) console.error(error);
    await idle(2000);
  }
}
process.exit(0);
```

Session request or stream errors leave the ticket unacknowledged so redelivery can reconnect. Failed or cancelled turns go to `support-tickets-failures` before the original ticket is acknowledged.

Connect the [failure collector](https://vercel.com/kb/guide/vercel-daemons-eve-agents#how-do-you-run-an-agent-for-every-message-in-a-queue) before deploying. It saves records to Postgres before the failure topic's seven-day retention expires.

Creation deduplication applies to the same authenticated caller while the session remains resumable. Concurrent creates can return different IDs during startup, and an ended session frees its `operationId` for reuse.

Check durable job outcomes before starting work again after a session ends. Follow-up messages retain the most recent 1,024 `idempotencyKey` values. See the [eve channel reference](https://eve.dev/docs/channels/eve#start-and-continue-a-session) for the full contract.

The full guide also demonstrates these worker patterns:

- Keep a Postgres `LISTEN` connection open to start a fraud review whenever your database flags an order.
  
- Rewrite 12,000 product descriptions, with one session per product.
  
- Check error rates every 15 seconds and investigate threshold breaches.
  

### How do you run Mastra workers with Vercel Daemons?

Run Mastra's orchestration and background-task workers as a daemon, using Mastra's split-process setup. The Mastra API keeps serving HTTP requests, and the worker pulls tasks from a shared PubSub backend. Configure both processes with the same storage and PubSub:

- **Shared state:** Configure both processes with the same remote storage and pull-capable PubSub. For example, use `PostgresStore` from `@mastra/pg` and `RedisStreamsPubSub` from `@mastra/redis-streams`.
  
  Set matching `DATABASE_URL` and `REDIS_URL` values.
  
- **API process:** Set `MASTRA_WORKERS=false` so the API doesn't run workers itself.
  
- **Worker callbacks:** Set `MASTRA_STEP_EXECUTION_URL` to the worker-callable API URL, including `/api`, and `MASTRA_WORKER_AUTH_TOKEN` to a bearer token the API accepts. The endpoint must also be reachable through Deployment Protection. If the main API requires Passport visitor identity, use a separate API deployment with machine authentication for callbacks.
  
- **Scheduler:** Leave Mastra's scheduler out of the daemon. It needs exclusive ownership and can run jobs twice while two deployments overlap.
  

Exclude `.env*`, `node_modules`, `.mastra`, and `.vercel` in the application's `.dockerignore`. Keep database, PubSub, and worker authentication credentials in the project's production environment variables.

Build the worker with `mastra worker build` inside the container, so its dependencies match the Linux runtime. Save this file as `Dockerfile.worker` at the application root:

```dockerfile
FROM node:24-slim AS build
WORKDIR /app
RUN corepack enable
COPY . .
RUN pnpm install --frozen-lockfile
RUN pnpm exec mastra worker build --output-dir .mastra/worker

FROM node:24-slim
WORKDIR /app
COPY --from=build /app/.mastra/worker/ ./
CMD ["sh", "-c", "MASTRA_WORKERS=orchestration exec node index.mjs"]
```

Then declare the daemon in `vercel.json`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "daemons": {
    "mastra-worker": {
      "root": ".",
      "entrypoint": "Dockerfile.worker",
      "instances": { "iad1": 1 },
      "terminationGracePeriodSeconds": 60
    }
  }
}
```

To run background tasks instead of orchestration, set `MASTRA_WORKERS=backgroundTasks` in the `CMD`. Follow the [Mastra worker setup](https://vercel.com/docs/frameworks/agents/mastra#run-background-workers-with-vercel-daemons) for authentication and shutdown details.

### What applies to every agent framework on Vercel Daemons?

- **Production only:** Vercel Daemons run only in production during the beta, so test workers locally with Docker.
  
- **Idempotent handlers:** Restarting a daemon doesn't checkpoint an agent's progress, and a retried job can repeat a tool call. Pass stable idempotency keys to every API that changes data.
  
- **OIDC from a file:** Read `VERCEL_OIDC_TOKEN_FILE` with `@vercel/oidc` 4.0.0 or later, or `vercel-oidc` 0.10.0 or later in Python. Vercel refreshes the file, so don't copy the token into a long-lived environment variable. Check that SDKs making authenticated calls use a file-aware OIDC dependency too.
  
- **Concurrency and budgets:** Instance and loop counts control how many jobs the workers handle at once. Agent sessions can continue after a worker disconnects, and deployments overlap, so a strict limit needs shared permits held until each session settles.
  
  Set model rate limits and per-session usage limits separately.
  

## How do you move existing workers to Vercel?

Vercel Daemons can run existing pull-based workers in containers. Check broker connectivity, Linux `amd64` dependencies, shutdown behavior, and any local state before moving the worker. Store persistent state in a database or object store.

You can move work gradually by having both platforms consume compatible jobs from the same broker. If the broker is also changing, migrate it separately and account for jobs already queued.

### Migration guides by platform

| Coming from                                          | Migration guide                                                                                                                    |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Render background workers                            | [Migrate Render background workers to Vercel Daemons](https://vercel.com/kb/guide/migrate-render-workers-to-vercel-daemons)        |
| Railway workers                                      | [Migrate Railway workers to Vercel Daemons](https://vercel.com/kb/guide/migrate-railway-workers-to-vercel-daemons)                 |
| Fly.io worker process groups                         | [Migrate Fly.io worker processes to Vercel Daemons](https://vercel.com/kb/guide/migrate-fly-io-workers-to-vercel-daemons)          |
| Google Cloud Run services, jobs, and worker pools    | [How to migrate from Google Cloud Run to Vercel](https://vercel.com/kb/guide/migrate-cloud-run-to-vercel)                          |
| AWS containers and workers                           | [Migrate self-hosted Next.js and containers from AWS to Vercel](https://vercel.com/kb/guide/migrate-containers-from-aws-to-vercel) |
| Heroku Python APIs, worker dynos, and Scheduler jobs | [Migrate a Python API from Heroku to Vercel](https://vercel.com/kb/guide/migrate-a-python-api-from-heroku-to-vercel)               |

The Heroku guide moves `worker` dynos to Vercel Queues and Heroku Scheduler jobs to Cron Jobs. To keep the Celery worker process instead, follow the Celery example in the Vercel Daemons section of this guide.

### Migration guides by worker framework

| Framework                                     | Where it runs on Vercel                           | Guide                                                                                    |
| --------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Celery, Sidekiq, BullMQ, and Temporal workers | Vercel Daemons, using the existing worker process | [The Complete Guide to Vercel Daemons](https://vercel.com/kb/guide/vercel-daemons)       |
| Celery tasks, rewritten for push delivery     | Vercel Queues and Vercel Functions                | [Run Celery tasks on Vercel Queues](https://vercel.com/docs/frameworks/backend/celery)   |
| Dramatiq                                      | Vercel Queues and Vercel Functions                | [Deploy Dramatiq workers on Vercel](https://vercel.com/docs/frameworks/backend/dramatiq) |

Celery has two paths. Run the existing worker as a daemon to retain its broker and task model, after checking the migration requirements above. Or adapt tasks to Vercel Queues so Vercel invokes and scales each consumer.

## What does background work cost on Vercel?

Vercel Functions and Vercel Daemons bill for Active CPU and Provisioned Memory. Active CPU billing pauses while code waits on I/O. Vercel Queues adds API-operation charges, and Vercel Workflows adds event and data charges.

| Option                    | What you pay for                                                                                                                   | Pricing page                                                                       |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `waitUntil` and Cron Jobs | Function compute only, including the time spent after the response                                                                 | [Functions usage and pricing](https://vercel.com/docs/functions/usage-and-pricing) |
| Vercel Queues             | API operations (send, receive, delete, visibility change, and notify), metered in 4 KiB chunks, plus compute for push consumers    | [Queues pricing](https://vercel.com/docs/queues/pricing)                           |
| Vercel Workflows          | Workflow Events, Workflow Data Written, and Workflow Data Retained, plus function compute and the Queues operations Workflows uses | [Workflows pricing](https://vercel.com/docs/workflows/pricing)                     |
| Vercel Daemons            | Active CPU and Provisioned Memory for every hour each instance runs                                                                | [Daemons limits](https://vercel.com/docs/daemons/limits)                           |
| Vercel Sandbox            | Active CPU, Provisioned Memory, sandbox creations, billable data transfer, snapshot storage, and drive storage, reads, and writes  | [Sandbox pricing](https://vercel.com/docs/sandbox/pricing)                         |

Several billing details change which option is cheaper:

- **Idle daemons still use memory:** An idle daemon instance uses no Active CPU, but its Provisioned Memory counts every hour it runs. One 2 GiB instance running all month uses about 1,460 GB-hours.
  
- **Hobby daemon quotas:** Hobby teams get 5 Active CPU-hours and 420 GB-hours of Provisioned Memory per month for daemons across all projects. Vercel stops all of the team's daemons when either quota runs out, so use Pro for workers that run all month.
  
- **Queue options cost extra:** Sends with an idempotency key, and push deliveries with a max concurrency, bill at 2x units for that operation.
  
- **Failed deliveries cost compute:** Push deliveries that time out bill the consumer function's full `maxDuration` at its configured memory. Set `maxDeliveries` and a retry depth alert to limit runaway retries.
  
- **Workflow steps produce events:** Every normal step produces three events, and each retry adds one more. Hobby includes 50,000 events per month.
  
- **Agent runs add model charges:** Daemons running AI agents also incur model usage, for example through AI Gateway. Control throughput with worker concurrency, and set model budgets and per-session usage limits separately.
  

## How do you handle failures and duplicates in background jobs on Vercel?

Assume every background job can run more than once. Vercel Queues delivers each message at least once, Vercel Workflows retries steps, and a daemon instance can stop mid-job during a deployment. Design each job so a second run produces the same result as the first.

### Make consumers idempotent

- **Deduplicate publishes:** Pass an idempotency key to `send()`. Vercel Queues drops a repeat publish with the same key for the shorter of 24 hours and the message's retention period.
  
- **Use stable job keys:** Pass keys such as `charge-${orderId}` to payment, email, and shipping APIs that support idempotency.
  
- **Write by key, not by increment:** Use upserts or unique constraints, and set values rather than adding to them.
  
- **Acknowledge last:** Acknowledge a queue message only after the work succeeds. An unacknowledged message becomes visible again after its visibility timeout. The JavaScript SDK's `handleCallback` sets a 5-minute timeout and extends it while the handler runs, and the Queues API default is 60 seconds.
  

### Handle messages that keep failing

Vercel Queues has no built-in dead-letter queue. `maxDeliveries` stops further deliveries without saving the payload elsewhere. To preserve failed jobs, record the payload durably inside the handler before it returns successfully.

The handler saves a failure record after 10 unsuccessful fulfillment attempts:

```typescript
import { handleCallback } from '@vercel/queue';
import { fulfillOrder, recordFailedOrder } from '@/lib/orders';

type Order = Parameters<typeof fulfillOrder>[0];

export const POST = handleCallback<Order>(
  async (order, metadata) => {
    try {
      await fulfillOrder(order);
    } catch (error) {
      if (metadata.deliveryCount < 10) throw error;

      // Application helper: durably upsert by messageId before acknowledging.
      await recordFailedOrder({
        messageId: metadata.messageId,
        order,
        error: String(error),
      });
    }
  },
  {
    retry: (_error, metadata) => ({
      afterSeconds: Math.min(300, 2 ** metadata.deliveryCount * 5),
    }),
  },
);
```

Implement `recordFailedOrder` as a durable upsert keyed by `messageId`. If it fails, the handler throws and the message remains eligible for retry. Leave `maxDeliveries` unset for this example so it does not cut off attempts to save the failure record.

Retries still end when the message expires, so monitor failed-record writes and keep the original job in durable application storage if it must survive queue expiry. See the [Python Queues guide](https://vercel.com/kb/guide/python-and-vercel-queues) for another failure-handling example.

### Stop a failing deployment's consumers

In push mode, messages stay pinned to the deployment that published them, so promoting a fix doesn't stop deliveries to the old deployment. Its consumers keep running and retrying for up to the message retention period, 24 hours by default.

Deleting the old deployment stops its consumers immediately, but the replacement deployment will not automatically process its pending messages. Preserve any jobs you need to replay before deletion; otherwise they remain queued until expiry. If the deployment also owns workflow runs, cancel those runs before deleting it.

Pending messages use no function compute after deletion. In Observability, group function duration by deployment to identify the consumers that are still running.

### Shut daemons down cleanly

The `terminationGracePeriodSeconds` setting controls how long Vercel waits after `SIGTERM` during a rollout, scale-down, or removal. Stop taking new work and finish the current job within that window.

Make retries safe and leave unfinished messages unacknowledged, since crashes and liveness-triggered restarts can interrupt work immediately. During deployments, overlapping versions must accept compatible messages.

## What are the limits for background work on Vercel?

The function max duration limits `waitUntil`, Cron Jobs, Vercel Queues push consumers, and each Vercel Workflows step. Vercel Workflows runs and Vercel Daemons have no overall duration limit.

### Vercel Functions max duration

These limits apply with Fluid compute, the default for new projects.

| Plan       | Default | Maximum | Extended maximum (beta)                                |
| ---------- | ------- | ------- | ------------------------------------------------------ |
| Hobby      | 300s    | 300s    | Not available                                          |
| Pro        | 300s    | 800s    | 1,800s for supported Node.js, Bun, and Python runtimes |
| Enterprise | 300s    | 800s    | 1,800s for supported Node.js, Bun, and Python runtimes |

During the beta, durations above 800 seconds must be set per function, and Secure Compute and Static IPs don't support them. See [Configuring maximum duration](https://vercel.com/docs/functions/configuring-functions/duration).

### Vercel Queues limits (public beta)

| Limit                              | Value                                                                                                            |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Message retention                  | 60 seconds to 7 days, 24 hours by default                                                                        |
| Delivery delay                     | Up to 7 days, capped at retention                                                                                |
| Visibility timeout                 | Up to 60 minutes. 60 seconds by default in the API, and 5 minutes with automatic extension in the JavaScript SDK |
| Max message size                   | 100 MB                                                                                                           |
| Max concurrency per consumer group | Unlimited by default                                                                                             |
| Idempotency key window             | The shorter of 24 hours and the message's retention                                                              |

See [Queues pricing and limits](https://vercel.com/docs/queues/pricing#limits).

### Vercel Workflows limits

| Limit                               | Value                                 |
| ----------------------------------- | ------------------------------------- |
| Run duration and `sleep` duration   | No limit                              |
| Step duration                       | The function max duration             |
| Steps per run                       | 10,000 on the default Node.js runtime |
| Events per run                      | 25,000 on the default Node.js runtime |
| Payload size per run, step, or hook | 50 MB                                 |

See [Workflows pricing and limits](https://vercel.com/docs/workflows/pricing).

### Vercel Daemons limits (public beta)

| Limit                             | Value                                                                                                                    |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Environments                      | Production only                                                                                                          |
| Instances per region              | 0 to 100 configured per daemon; subject to project and team quotas                                                       |
| Instances per project environment | 50 total across daemons and regions by default                                                                           |
| Daemons per project               | 100                                                                                                                      |
| Memory and CPU per instance       | Hobby: 1 or 2 GiB with 1 vCPU. Pro and Enterprise also support 4 GiB with 2 vCPUs and 8 GiB with 4 vCPUs. Default: 2 GiB |
| Shutdown grace period             | 30 seconds by default, up to 3,600 seconds                                                                               |
| Inbound traffic                   | Not supported                                                                                                            |
| Project root directory            | Must be the repository root                                                                                              |
| Pro concurrent team quota         | 16 vCPUs and 32 GiB across projects by default                                                                           |

See [Daemons limits](https://vercel.com/docs/daemons/limits).

### Vercel Sandbox limits

Vercel Sandbox sessions run for 5 minutes by default, and up to 45 minutes on Hobby or 24 hours on Pro and Enterprise. See [session duration and persistence](https://vercel.com/kb/guide/vercel-sandbox-duration-and-persistence) and [Sandbox pricing and limits](https://vercel.com/docs/sandbox/pricing).

## Next steps

- Choose a compute type with [Functions, containers, and Daemons](https://vercel.com/kb/guide/vercel-functions-containers-daemons).
  
- Build [Next.js background jobs](https://vercel.com/kb/guide/how-to-run-background-jobs-in-nextjs-on-vercel) with [Vercel Workflows](https://vercel.com/docs/workflows).
  
- Build [Python queue consumers](https://vercel.com/kb/guide/python-and-vercel-queues) and review [how delivery works](https://vercel.com/docs/queues/concepts).
  
- [Configure always-on workers](https://vercel.com/kb/guide/vercel-daemons) with Vercel Daemons.
  
- Run background agents with [eve on Vercel Daemons](https://vercel.com/kb/guide/vercel-daemons-eve-agents) or [Mastra on Vercel](https://vercel.com/docs/frameworks/agents/mastra#run-background-workers-with-vercel-daemons).
  
- Schedule jobs with [Cron Jobs](https://vercel.com/docs/cron-jobs).
  
- Run untrusted code with [Vercel Sandbox](https://vercel.com/docs/sandbox).

## Related Documentation

- [Vercel Daemons](https://vercel.com/docs/daemons)
- [eve](https://eve.dev/docs)

## More Vercel Daemons guides

- [Use Vercel Daemons with eve agents](/kb/guide/vercel-daemons-eve-agents): Connect eve agents to Vercel Daemons to process queued jobs, offload heavy work, and react to events. Includes code, authentication, and shutdown handling.
- [Migrate Fly.io worker processes to Vercel Daemons](/kb/guide/migrate-fly-io-workers-to-vercel-daemons): Migrate Fly.io workers to Vercel Daemons with your existing Dockerfile. Map process settings, preserve queue state, and plan cutover and rollback.
- [Migrate Render background workers to Vercel Daemons](/kb/guide/migrate-render-workers-to-vercel-daemons): Migrate Render workers to Vercel Daemons. Translate render.yaml, package Celery in Docker, prepare the broker, and verify the queue handoff.