You are an expert in Vercel Daemons, the long-running background processes that run from a container image in the same project as an app.
Daemons are in Public Beta on all plans. Say so when you recommend them.
What It Is
A daemon is a named resource in a project that runs a fixed number of identical instances of one container image in each region you list. Vercel builds the image from the daemon's Dockerfile or Containerfile, rolls it out when the deployment becomes the production deployment, restarts instances that exit or crash, and forwards stdout/stderr to the project's Logs. Daemons run on Fluid compute.
Daemons cannot receive inbound traffic. They pull work from a queue, a workflow engine, or an external API.
When to Use Daemons
Use a daemon when the process owns a continuous loop that retrieves work.
| Need | Use | Why |
|---|---|---|
| Always-on queue consumer (Vercel Queues poll mode, SQS, Kafka, Redis), Celery/Sidekiq/BullMQ worker, workflow-engine worker, self-hosted CI runner | Daemons | Long-running loop, fixed instances per region, no request needed |
| HTTP APIs, webhooks, streaming, WebSockets | Vercel Functions (⤳ skill: vercel-functions) |
Daemons can't receive traffic |
| Fire-and-forget background job, fan-out, buffering with Vercel invoking your handler | Vercel Queues push mode (⤳ skill: queues) |
Serverless, scales automatically, works in previews |
| Durable multi-step logic with sleep, hooks, or approvals | Vercel Workflows (⤳ skill: workflow) |
Durable steps and replay |
| Run code on a schedule | Cron Jobs (⤳ skill: vercel-functions) |
Time-based trigger |
| Run untrusted or AI-generated code in isolation | Vercel Sandbox (⤳ skill: vercel-sandbox) |
Ephemeral microVMs |
| Multiple frontends and backends in one project | Vercel Services (⤳ skill: vercel-services) |
Daemons sit next to services; they are not services |
If the user's work is request-driven, prefer Functions or Queues push mode. Reach for a daemon only when something must stay running.
Quick Start
Add a Dockerfile in the worker's source directory:
# worker/Dockerfile
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "worker.js"]
Declare the daemon under the top-level daemons key:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"daemons": {
"worker": {
"root": "worker/",
"entrypoint": "Dockerfile",
"instances": { "iad1": 2 }
}
}
}
The same configuration works in vercel.toml as a [daemons.worker] table. Deploy to production. Vercel builds the image, pushes it to Vercel Container Registry as daemon.worker, and starts two instances in iad1.
Daemons are always declared at the top level, never inside services. They run alongside an existing app (Next.js, Services, or anything else) without changing how Vercel builds it.
Configuration Reference
| Field | Type | Required | Notes |
|---|---|---|---|
root |
string |
Yes | Source directory, relative to vercel.json. Must stay inside the project. Files under it are not published as static assets |
entrypoint |
string |
Yes | Path to the Dockerfile or Containerfile, relative to root. Its directory is the build context |
instances |
Record<region, number> |
Yes | Instances per region ID, each 0–100. Vercel runs only in the listed regions |
command |
string[] |
No | Replaces the image's ENTRYPOINT and CMD. Not run through a shell |
resources |
{ requests: { memory } } |
No | 1Gi (1 vCPU), 2Gi (1 vCPU, default), 4Gi (2 vCPU), 8Gi (4 vCPU) |
instancesvsreplicas: the publishedvercel.jsonschema and the Build Output API useinstances.replicasis a deprecated alias that is ignored wheninstancesis set. If the project already usesreplicas, leave it working, but writeinstancesin new configuration.- Names: lowercase letters only, joined by
_,__, or one or more-(worker,email_sender,celery-beat; notWorker,worker1,_worker), 64 characters max. Two names that collapse to the same image repository once repeated hyphens are merged, such asa-banda--b, fail the build. A daemon and a service may share a name. - Rejected fields: service fields such as
framework,runtime,buildCommand, andbindingsfail validation on a daemon. - Build: images are built for Linux
amd64. Deployment environment variables are passed to the image build as build arguments. - Shell features:
commandis exec-form. Use["sh", "-c", "exec node worker.js --id $WORKER_ID"]for variable expansion. - No
CMD: the build fails if the image has neitherENTRYPOINTnorCMDandcommandis not set.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"daemons": {
"celery": {
"root": "backend/",
"entrypoint": "Dockerfile",
"command": ["celery", "-A", "app", "worker", "--loglevel=info"],
"instances": { "iad1": 2, "cdg1": 1 },
"resources": { "requests": { "memory": "4Gi" } }
}
}
}
Lifecycle and Scaling
- Production only during the beta. Daemons run only for the production deployment. Preview deployments do not start daemons; test changes by running the image locally.
- Rollout: when a deployment becomes production (deploy, promote, Instant Rollback, or a completed rolling release), Vercel starts the new instances and sends
SIGTERMto the previous deployment's instances. For a short time both versions run at once, so both must be able to consume the same queue and tolerate the same database schema. - Image, command, and environment variables belong to the deployment. Change them with a new production deployment.
- Scaling with
vercel.json: changeinstancesand deploy to production. The map replaces the previous one; regions removed from it stop. - Scaling overrides: in the project dashboard, Daemons → select the daemon → Set Scaling Overrides. Overrides persist across deployments and win over
instancesuntil cleared with Clear scaling overrides. If avercel.jsonchange toinstancesseems ignored, check for an override first. There is no autoscaling, and scaling from the CLI or API is not available yet. - Stop without deleting: set every region to
0orinstancesto{}, or set a0override from the dashboard. - Delete: remove the daemon from
daemonsand deploy to production. - Paused projects stop all daemon instances; they start again when the project resumes.
Graceful Shutdown
Vercel sends SIGTERM on rollout, scale-down, removal, and manual restart, then waits 10 seconds by default before terminating the process. Stop taking new work immediately and finish the job in progress. Instances can stop at any time, so jobs must be idempotent and safe to retry; acknowledge a queue message only after processing it.
// worker/worker.ts
const shutdown = new AbortController();
process.on('SIGTERM', () => shutdown.abort());
async function main(): Promise<void> {
while (!shutdown.signal.aborted) {
await processNextJob(shutdown.signal); // bounded poll; back off when there is no work
}
process.exit(0);
}
void main();
# worker/worker.py
import signal
from threading import Event
shutdown = Event()
signal.signal(signal.SIGTERM, lambda signum, frame: shutdown.set())
while not shutdown.is_set():
process_next_job(shutdown) # bounded poll, finish the current job before returning
Consuming Vercel Queues From a Daemon
A daemon is the long-running consumer for Vercel Queues poll mode. Use PollingQueueClient from @vercel/queue with an explicit region, because messages can only be received from the region they were sent to:
// worker/worker.ts
import { PollingQueueClient } from '@vercel/queue';
const { receive } = new PollingQueueClient({ region: 'iad1' });
const shutdown = new AbortController();
process.on('SIGTERM', () => shutdown.abort());
while (!shutdown.signal.aborted) {
const result = await receive('orders', 'fulfillment', async (message) => {
await processOrder(message); // acknowledged when this resolves, retried if it throws
}, { limit: 10 });
if (!result.ok && result.reason === 'empty') {
await new Promise((r) => setTimeout(r, 1000));
}
}
Run the daemon in the same region the producer sends to. Topics are partitioned by deployment ID by default; read the poll-mode versioning section before relying on cross-deployment consumption. For the producer side and push-mode consumers, load ⤳ skill: queues.
Environment Variables and OIDC
- Each instance receives the deployment's environment variables.
- Each instance receives an OIDC token written to a file whose path is in
VERCEL_OIDC_TOKEN_FILE, refreshed periodically. Read it with@vercel/oidc4.0.0 or later (Node.js) orvercel-oidc0.10.0 or later (Python). Older SDK versions that only readVERCEL_OIDC_TOKENwill not find it.
Local Development
vercel dev does not run daemons; it prints a warning and starts the rest of the project. Run the daemon with Docker instead:
vercel env pull # writes .env.local
docker build -t worker worker/
docker run --env-file .env.local worker
Logs and Observability
Daemon output appears in the project's Logs with source daemon and the daemon's name, so you can filter to one daemon. The project dashboard's Daemons tab shows each daemon's instances, status, and logs.
Build Output API
With vercel build or a custom builder, .vercel/output/config.json lists daemons as "daemons": { "worker": {} } (the complete set; omitted daemons are stopped), and each has .vercel/output/daemons/<name>/.vc-config.json with runtime: "container", an image already in Vercel Container Registry, instances, and optional command and resources. vercel build builds and pushes the images and writes these files for you.
Limits
| Limit | Value |
|---|---|
| Instances per region | 100 per daemon |
| Memory per instance | 1, 2 (default), 4, or 8 GiB |
| Shutdown grace period | 10 seconds after SIGTERM by default |
| CPU architecture | Linux amd64 |
Default per-team quotas apply during the beta: Hobby is limited to 1–2 GiB instances and a monthly vCPU-hour allowance (instances stop when it runs out), Pro allows all sizes up to a concurrent vCPU and memory cap, and Enterprise is custom. Check Daemons limits for the current numbers before sizing a deployment.
Not supported during the beta: preview deployments, inbound HTTP or TCP traffic, autoscaling, persistent local storage (use Vercel Blob or a Marketplace database), running in vercel dev, being the target of a service binding or rewrite, builds without a Dockerfile/Containerfile, and projects whose root directory is a subfolder such as apps/web.
Common Pitfalls
- Expecting traffic: a daemon that listens on a port receives nothing. Serve requests with Functions or Services, and have the daemon pull work.
- Testing on a preview: daemons never start for preview deployments during the beta. Deploy to production or run the image locally.
- Ignored scaling change: a dashboard scaling override wins over
instancesuntil it is cleared. - Unsafe overlap: old and new versions run side by side during a rollout. Keep message formats and schema migrations backward compatible.
- No SIGTERM handling: the process is killed 10 seconds after
SIGTERM. Make jobs idempotent and acknowledge only after completion. - Shell syntax in
command:$VARand&&are not expanded. Wrap insh -c. - Monorepo root directory: projects with a subfolder root directory cannot add daemons yet.
- Stale OIDC SDK: use
@vercel/oidc≥ 4.0.0 orvercel-oidc≥ 0.10.0 so the token file is read.
References
- 📖 docs: https://vercel.com/docs/daemons
- 📖 configuration reference: https://vercel.com/docs/daemons/config-reference
- 📖 managing daemons: https://vercel.com/docs/daemons/managing-daemons
- 📖 limits and quotas: https://vercel.com/docs/daemons/limits
- 📖 Build Output API: https://vercel.com/docs/build-output-api/daemons
- 📖 Queues poll mode: https://vercel.com/docs/queues/poll-mode
- 📖 Functions vs containers vs Daemons: https://vercel.com/kb/guide/vercel-functions-containers-daemons