---
title: Migrate Railway workers to Vercel Daemons
description: Move Railway workers to Vercel Daemons with Docker configuration, broker migration, shutdown testing, a gradual queue handoff, and rollback.
url: "https://vercel.com/kb/guide/migrate-railway-workers-to-vercel-daemons"
published: 2026-10-26
last_updated: 2026-10-26
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

To move always-on Railway workers to [Vercel Daemons](https://vercel.com/kb/guide/vercel-daemons), package each worker with a Dockerfile, declare it in `vercel.json`, and deploy to production. Start with zero instances, test shutdown behavior, and move processing gradually while Railway and Vercel use the same queue broker.

Consider an orders project on Railway. Its Next.js app adds jobs to Redis, and a BullMQ worker processes payments and sends receipts. Separate cron jobs clean up abandoned carts. The worker can move to a daemon while the [Railway migration guide](https://vercel.com/kb/guide/migrate-railway-to-vercel) covers the web app, databases, and DNS.

Vercel builds the worker image and maintains the instance count you configure in each region. Daemons pull work through outbound connections and restart after the process exits, including a clean exit.

> Vercel Daemons is in [Public Beta](https://vercel.com/docs/release-phases#public-beta) on all plans.

## Prerequisites

- Access to the Railway project and the [Railway CLI](https://docs.railway.com/cli), linked to it.
  
- Vercel project whose [root directory](https://vercel.com/docs/builds/configure-a-build#root-directory) is the repository root. Daemons don't support subfolder roots such as `apps/web` during the beta.
  
- Permission to deploy to production, since that's the only environment that starts daemons.
  
- [Vercel CLI](https://vercel.com/docs/cli) 64.3.0 or later, linked to the intended Vercel project for the `vercel daemons` commands.
  
- [Docker](https://docs.docker.com/get-docker/), to build and test worker images locally.
  
- Team capacity within the [Daemons quotas](https://vercel.com/docs/daemons/limits#default-quotas). Hobby allows 5 Active CPU-hours per month across the team; assess actual CPU usage and instance size rather than elapsed running time.
  

## Should your worker become a daemon?

Check these requirements before moving a Railway worker:

- **Pulls its own work.** Queue consumers and polling workers can fit this model. Move inbound HTTP handlers to a [Vercel Function or Service](https://vercel.com/kb/guide/vercel-functions-containers-daemons); other inbound protocols need a host that supports them.
  
- **Stays running.** Daemons restart after any exit. Scheduled tasks that finish and exit belong in [cron jobs](https://vercel.com/docs/cron-jobs).
  
- **Tolerates ephemeral storage.** Move durable files from Railway volumes to a database or [Vercel Blob](https://vercel.com/docs/vercel-blob), and update the worker to use that storage.
  
- **Builds for Linux** `amd64`**.** Supply a `Dockerfile` or `Containerfile` and compatible dependencies.
  
- **Fits the available resources.** During the beta, an instance can have up to 8 GiB of memory and 4 vCPUs. Add instances when your workload supports parallel processing.
  

For tasks that fit within [Function duration limits](https://vercel.com/docs/functions/limitations), [Vercel Queues](https://vercel.com/docs/queues) can deliver messages to a Function. Daemons let you retain a continuously running worker and its queue library, with changes to networking, storage, and shutdown.

## How Railway worker concepts map to Vercel

Most of a Railway worker's settings have a direct home in the daemon's entry in `vercel.json`. The rest move to project settings or to a Marketplace provider.

Both platforms must reach the same broker for the gradual worker handoff in this guide. Moving the broker is a separate data migration, covered in step 2.

| Railway                                    | Vercel                                                                                                                                        |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Worker service with no public domain       | A named entry under [`daemons`](https://vercel.com/docs/daemons/config-reference) in `vercel.json`                                            |
| Railpack or Nixpacks build                 | A `Dockerfile` or `Containerfile` in the daemon's `root` (required)                                                                           |
| Same repo, different `startCommand`        | [`command`](https://vercel.com/docs/daemons/config-reference#command), which overrides the image's `ENTRYPOINT` and `CMD`                     |
| Replicas                                   | [`instances`](https://vercel.com/docs/daemons/config-reference#instances), a count per region from 0 to 100                                   |
| Multi-region replicas                      | `instances` keyed by Vercel [region ID](https://vercel.com/docs/regions), such as `{ "iad1": 2, "cdg1": 1 }`                                  |
| CPU and memory limits in service settings  | [`resources.requests.memory`](https://vercel.com/docs/daemons/config-reference#resources): 1, 2, 4, or 8 GiB, with 1 to 4 vCPUs               |
| Restart policy                             | Built in: Vercel restarts instances that exit or crash                                                                                        |
| Deployment draining time                   | [`terminationGracePeriodSeconds`](https://vercel.com/docs/daemons/config-reference#terminationgraceperiodseconds), 30 by default, up to 3,600 |
| Service and reference variables            | Project [environment variables](https://vercel.com/docs/environment-variables) in the Production scope                                        |
| Private networking at `*.railway.internal` | Connection strings from your providers, such as [Marketplace](https://vercel.com/marketplace) Redis and Postgres                              |
| Redis, Postgres, or RabbitMQ services      | Managed providers from the Marketplace or elsewhere                                                                                           |
| Volumes                                    | Vercel Blob or a database                                                                                                                     |
| Cron service                               | A [cron job](https://vercel.com/docs/cron-jobs) that calls a route; an in-process scheduler needs protection against overlapping instances    |
| PR environments for the worker             | None during the beta; test the image locally with Docker                                                                                      |
| Service logs and metrics                   | [Logs](https://vercel.com/docs/logs/runtime) filtered by `source:daemon`, plus CPU and memory charts on the daemon's page                     |
| Scaling replicas from the dashboard        | Scaling overrides in the dashboard or `vercel daemons scale`                                                                                  |

Daemons accept no inbound connections and run only in production deployments during the beta. Replace Railway private-network dependencies before starting a daemon, and test the image locally before its first production run.

## Steps

Keep the Railway worker available through validation and the rollback window. Coordinate production app, database, and broker changes separately; deploying with zero daemon instances postpones only the Vercel worker's start.

### 1\. Inventory the Railway project

List every service and decide where it lands before you change any code. For the orders project, the inventory looks like this:

| Railway service | What it does                      | Destination on Vercel                                                                                         |
| --------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `web`           | Next.js app, public domain        | Framework detection, per the [Railway migration guide](https://vercel.com/kb/guide/migrate-railway-to-vercel) |
| `worker`        | BullMQ consumer, no public domain | A daemon                                                                                                      |
| `cleanup`       | Cron service that runs and exits  | A [cron job](https://vercel.com/docs/cron-jobs)                                                               |
| `Redis`         | Queue broker                      | A Redis provider from the Marketplace                                                                         |
| `Postgres`      | Orders database                   | A Postgres provider from the Marketplace                                                                      |

For each worker, write down its start command, replica count and regions, CPU and memory limits, the variables it reads, and any path it writes to on disk. Export its variables with the Railway CLI so you have them in one place:

```bash
umask 077
railway variable list --service worker --environment production --kv > .env.railway
```

Keep `.env.railway` out of version control and delete it when the migration ends. Railway omits sealed variables from this export, so collect those from the system that issued them.

### 2\. Prepare a shared broker and migrate its state

Use the [Railway migration guide](https://vercel.com/kb/guide/migrate-railway-to-vercel) for the web app, Postgres, and volumes. Before moving the worker, make sure the production app and both worker deployments can reach the same broker. Railway's `*.railway.internal` hostnames resolve only within its private network.

If the broker must move, choose a provider that supports your queue library. BullMQ needs a Redis connection compatible with ioredis, including its blocking commands. Configure persistence for your durability requirements and use `maxmemory-policy=noeviction` so Redis does not evict queue data.

Complete the broker migration before the worker handoff. Pause producers and schedulers while you account for the old broker's work. Drain pending jobs with Railway workers or use a tested procedure to transfer queue state.

For a state transfer, stop the old consumers after their active jobs finish and before copying broker state. Include delayed jobs, scheduled retries, waiting dependencies, and recurring schedules; an empty waiting queue alone does not establish that the broker is ready to retire.

After checking that the destination contains the required state, connect the Railway worker and the production app to it. Resume producers and schedules, then confirm jobs complete. Keep the old broker available until you have reconciled its remaining work and tested the rollback plan.

### 3\. Give each worker a Dockerfile

Daemons build from a `Dockerfile` or `Containerfile`. For a worker previously built with Railpack, add a Dockerfile alongside its code and dependency files:

```text
orders/
├── vercel.json
├── package.json        # the Next.js app
├── app/
└── worker/
    ├── .dockerignore
    ├── Dockerfile
    ├── package.json
    ├── package-lock.json
    ├── orders.js       # existing processOrder implementation
    └── worker.js
```

Keep `bullmq` and `ioredis` in the worker's production dependencies and commit its lockfile. The JavaScript example below uses ES modules; set `"type": "module"` in `worker/package.json`. It assumes `orders.js` exports your existing `processOrder` function.

Use this `worker/Dockerfile`:

```docker
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "worker.js"]
```

Use `worker/.dockerignore` to exclude local dependencies and environment files from the build context:

```text
node_modules
.env*
.git
```

With the daemon's `root` set to `worker/`, the image can copy only files from that directory. If your Railway worker shares the web app's codebase and uses a different start command, choose one of these layouts:

- Move the worker's dependencies into `worker/`, or into a shared package it installs.
  
- Build from the repository root with `Dockerfile.worker`, `root: "./"`, and `entrypoint: "Dockerfile.worker"`. Use the suffixed filename so Vercel doesn't treat it as the web app's Dockerfile. Put the `.dockerignore` at the repository root for this layout, including the exclusions for `.env.railway` and `.env.docker`.
  

Python workers that share a backend image keep sharing it. Point the daemon at the backend's Dockerfile and override the start command, as step 5 shows.

### 4\. Make the worker stop cleanly

Vercel sends `SIGTERM` before replacing or removing an instance and waits 30 seconds by default before force-stopping it. In BullMQ, [`worker.close()`](https://docs.bullmq.io/guide/workers/graceful-shutdown) stops fetching jobs and waits for active processors to finish or fail. It has no timeout of its own, so allow enough termination time for the worker and its connections to close.

Adapt `worker/worker.js` to your existing handler:

```javascript
import { Worker } from 'bullmq';
import IORedis from 'ioredis';
import { processOrder } from './orders.js';

if (!process.env.REDIS_URL) {
  throw new Error('REDIS_URL is required');
}

const connection = new IORedis(process.env.REDIS_URL, {
  maxRetriesPerRequest: null,
});

const worker = new Worker(
  'orders',
  async (job) => processOrder(job.data),
  { connection, concurrency: 5 },
);

worker.on('error', (error) => {
  console.error('orders-worker error', error);
});

console.log(`orders-worker started in ${process.env.VERCEL_REGION ?? 'local'}`);

let stopping = false;
async function shutdown(signal) {
  if (stopping) return;
  stopping = true;
  console.log(`${signal} received, finishing active jobs`);

  try {
    await worker.close();
    await connection.quit();
    process.exit(0);
  } catch (error) {
    console.error('orders-worker shutdown failed', error);
    process.exit(1);
  }
}

process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('SIGINT', () => void shutdown('SIGINT'));
```

If an instance stops before BullMQ records completion, another worker can process the stalled job again. Make `processOrder` safe to repeat after partial progress. Use the payment provider's idempotency mechanism for charges and persist enough order state to avoid repeating completed actions.

BullMQ's [`jobId`](https://docs.bullmq.io/guide/jobs/job-ids) prevents adding a duplicate while that ID remains in the queue; it does not make the handler's side effects idempotent.

Keep message formats and database changes compatible with both worker versions during rollout and rollback. For other queue libraries, check when messages are acknowledged and what happens after forced termination. Celery, for example, acknowledges tasks before execution by default; its retry and acknowledgment settings need a separate review.

### 5\. Declare the daemon in vercel.json

Add a top-level `daemons` key next to anything already in `vercel.json`. Start with zero instances; step 10 raises the count when you're ready to hand off the queue:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "daemons": {
    "orders-worker": {
      "root": "worker/",
      "entrypoint": "Dockerfile",
      "instances": { "iad1": 0 },
      "resources": { "requests": { "memory": "2Gi" } },
      "terminationGracePeriodSeconds": 60
    }
  }
}
```

Translate the Railway settings you recorded in step 1:

- **Replicas become** `instances`**.** Two Railway replicas in one region become `{ "iad1": 2 }`. Choose the [Vercel region](https://vercel.com/docs/regions) closest to your broker and database, since every job makes round trips to both.
  
- **Resource limits become** `resources.requests.memory`**.** Pick 1, 2, 4, or 8 GiB; Vercel assigns 1, 1, 2, or 4 vCPUs to match.
  
- **Draining time becomes** `terminationGracePeriodSeconds`**.** Set it a little above your longest job, up to 3,600 seconds.
  
- **The name must be lowercase letters with separators.** `orders-worker` and `email_sender` work; `worker1` and `Worker` fail the build.
  

For a Celery worker that shares a backend image, use this alternative `vercel.json` entry. Its `command` replaces the image's `ENTRYPOINT` and `CMD`. Keep the count at zero until handoff, and substitute `celery` for `orders-worker` in later CLI commands:

```json
{
  "daemons": {
    "celery": {
      "root": "backend/",
      "entrypoint": "Dockerfile",
      "command": ["celery", "-A", "app", "worker", "--loglevel=info"],
      "instances": { "iad1": 0 }
    }
  }
}
```

Vercel doesn't run `command` through a shell, so `$VARIABLES` stay literal. Wrap the command in `["sh", "-c", "exec ..."]` when you need expansion.

### 6\. Move the worker's environment variables

Daemon instances receive the project's environment variables. Use `.env.railway` as an inventory, then add the application variables the worker needs in the Production scope. Replace old broker and database URLs with the destination values from step 2, and omit Railway-provided variables. Check existing project values before changing variables shared with the web app:

```bash
vercel env add REDIS_URL production --sensitive
vercel env add STRIPE_SECRET_KEY production --sensitive
```

Railway reference variables such as `${{Redis.REDIS_URL}}` don't resolve outside Railway. Replace each with the real value from the destination provider; Marketplace integrations add theirs to the project for you.

Vercel also supplies project variables as image build arguments. Keep runtime credentials out of Dockerfile `ARG` and `ENV` instructions because those values can persist in image metadata or layers. If the build needs private-package credentials, verify a supported secret-delivery method before deploying that image.

For providers that support [OIDC](https://vercel.com/docs/oidc), replace long-lived cloud keys with federation. Each instance's `VERCEL_OIDC_TOKEN_FILE` holds the path to its short-lived token.

### 7\. Recreate cron services and schedulers

Railway cron services start on a schedule and exit after completing their task. Move suitable tasks to a [Vercel cron job](https://vercel.com/docs/cron-jobs) that calls a protected route within Function limits. Longer tasks can be enqueued by that route for a daemon to process. Disable the Railway schedule before enabling its replacement so both platforms don't trigger the same work.

BullMQ repeatable jobs and Job Schedulers remain with their Redis queue state. When both deployments use that same state, either worker can process the scheduled jobs. If you moved Redis in step 2, verify that recurring schedules and delayed jobs moved with it before enabling them on the destination.

Celery beat and `node-cron` need a separate scheduler plan. Even with one configured daemon instance, old and new processes can overlap during a deployment. Use coordination that allows only one scheduler to enqueue each occurrence, or move the schedule to a cron route. Job handlers still need protection against repeated execution.

Celery beat's default scheduler stores last-run times in the local `celerybeat-schedule` file. Store that state durably before moving beat to an environment with ephemeral storage. Database-backed storage preserves it across instance replacement, but does not elect a single active scheduler.

### 8\. Test the worker locally with Docker

Daemons don't run in preview deployments or `vercel dev` during the beta. Test the image locally using a development broker and test credentials. Create `.env.docker` at the project root with one unquoted `KEY=value` per line, and exclude it from version control and Docker build contexts:

```text
REDIS_URL=rediss://default:dev-password@dev-redis.example.com:6379
STRIPE_SECRET_KEY=sk_test_example
```

From the project root, build and run the Linux `amd64` image:

```bash
docker build --platform linux/amd64 -t orders-worker worker/
docker run --rm --platform linux/amd64 --name orders-worker --env-file .env.docker orders-worker
```

Enqueue a test job against the development broker. While it is running, open another terminal and stop the container with the configured grace period:

```bash
docker stop -t 60 orders-worker
```

Check for `SIGTERM received` in the logs, verify that BullMQ records the job as completed, and confirm the container exits before the timeout. In a separate local test, interrupt a job before completion and verify stalled-job recovery. Repeat the same logical job to check that payments and other side effects are not duplicated.

### 9\. Deploy to production with zero instances

Check that every daemon entry has zero instances and no existing scaling override raises its count. Then deploy the project to build and register the images. This is a production app deployment too, so review any web-app changes included in it:

```bash
vercel --prod
```

Confirm Vercel knows about the daemon:

```bash
vercel daemons ls
vercel daemons inspect orders-worker
```

If the image build fails, use the [troubleshooting section](https://vercel.com/kb/guide/migrate-railway-workers-to-vercel-daemons#troubleshooting). Confirm that desired and running daemon counts are zero while the Railway worker processes jobs.

### 10\. Hand off the queue

For the BullMQ example, keep both deployments on the same Redis database, queue name, and BullMQ prefix. Use compatible worker versions and payload formats, and check that their combined concurrency stays within the broker's and downstream services' limits.

Start one Vercel instance without redeploying by applying a scaling override:

```bash
vercel daemons scale orders-worker iad1=1
```

Watch its logs until it has completed real jobs:

```bash
vercel logs --query 'source:daemon'
```

To follow only this daemon, filter your project's **Logs** page by `daemon:orders-worker`.

Increase Vercel capacity as you reduce Railway replicas. Compare completion rate, queue age, failures, and resource use at each change; equal replica counts can provide different throughput.

Allow Railway workers to finish active jobs, then stop their deployment after Vercel is handling the load. Disable automatic Railway redeployments during the rollback window so a push cannot restart the old workers.

Set `instances` in `vercel.json` to the count you tested, deploy to production, and clear the override so later deployments use the file:

```bash
vercel daemons scale orders-worker --reset
```

Check the resulting instance count. If the two platforms still use different brokers, stop here and complete the state migration in step 2 before retiring the Railway worker. Scaling workers does not transfer jobs between brokers.

### 11\. Verify under real load

Observe the daemon through a representative workload cycle, including scheduled tasks and peak traffic:

- **Instances.** Use `vercel daemons instances ls orders-worker` to compare running instances with the intended count. Investigate unexpected restarts using the logs and memory charts.
  
- **Job processing.** Compare completion rate, queue age, and failed or stalled jobs with the Railway baseline. Check broker connectivity and downstream errors or rate limits before adding concurrency.
  
- **Resources.** Review CPU, memory, running instances, and data transfer on the daemon's dashboard page. Investigate sustained memory growth and memory-related exits before increasing the instance size.
  

Restart one instance while processing a controlled test job to check graceful replacement. Replace `your_instance_id` with an ID from the instance list:

```bash
vercel daemons instances restart your_instance_id
```

Verify the job's recorded result and side effects. This checks a managed restart; use the local forced-stop test from step 8 to check interrupted-job recovery.

### 12\. Keep a rollback path, then retire Railway

Keep the Railway worker's code and settings available for the agreed rollback window, with automatic deployments disabled. To return processing to Railway, restore a compatible worker against the same broker and verify that it can complete jobs. Then scale the Vercel daemon to zero in every active region and confirm its instances have stopped. For the single-region example, use `vercel daemons scale orders-worker iad1=0`.

If the Vercel worker is causing harmful side effects, stop it first and allow jobs to wait while you restore Railway. In either sequence, confirm the old worker still understands the current job payloads and database schema. Jobs remain in the shared broker; a rollback to a different broker requires its own state migration.

[Instant Rollback](https://vercel.com/docs/instant-rollback) restores the earlier deployment's daemon definitions, images, and configured instance counts with the app. Existing scaling overrides remain in force, so inspect them before relying on the restored configuration.

After the rollback window and queue-state checks pass, delete the Railway worker service and remove `.env.railway` and `.env.docker` from your machine. Keep broker and database resources that other services still use.

## What doesn't map from Railway

Plan for these Daemons beta limits:

- **Preview environments.** Railway PR environments can run a copy of the worker; Vercel previews don't start daemons. Test images locally with Docker.
  
- **Inbound traffic.** Daemons cannot be reached over HTTP or TCP, a [service binding](https://vercel.com/docs/services/bindings), or a rewrite. If your Railway worker also served a health check, a Prometheus `/metrics` endpoint, or a queue dashboard such as Bull Board, move the dashboard to your app and push metrics instead of scraping them.
  
- **Autoscaling.** Instance counts are fixed per region. Change them in `vercel.json`, the dashboard, or the CLI.
  
- **Persistent disk.** Instance storage is ephemeral. Volumes move to Vercel Blob or a database.
  
- **Subfolder root directories.** Monorepos whose Vercel project root is a subfolder, such as `apps/web`, can't add daemons yet.
  
- **Non-**`amd64` **images.** Vercel builds for Linux `amd64`. Binaries you copy into the image must match.
  

During the beta, each team also has [default quotas](https://vercel.com/docs/daemons/limits#default-quotas):

| Plan       | Instance sizes     | Quota                                                         |
| ---------- | ------------------ | ------------------------------------------------------------- |
| Hobby      | 1 or 2 GiB, 1 vCPU | 5 Active CPU-hours per month across the team                  |
| Pro        | All sizes          | 16 vCPUs and 32 GiB of memory running at once across the team |
| Enterprise | All sizes          | Custom                                                        |

Each project environment runs at most 50 daemon instances across all its daemons and regions. On Pro and Enterprise, daemons are billed for Active CPU and Provisioned Memory; see [Pricing](https://vercel.com/pricing) for rates.

## Troubleshooting

### The worker logs `ENOTFOUND redis.railway.internal`

Check the Production environment variables for `*.railway.internal` hostnames. Replace them with the destination provider's reachable connection string, including its required TLS settings, and redeploy.

### Instances restart over and over

Read the logs before each restart and check memory usage. Look for missing variables, startup errors, dependency failures, or memory exhaustion. If the process exits successfully, confirm whether it is intended to stay running. Scheduled tasks that complete and exit belong in a [cron job](https://vercel.com/docs/cron-jobs); a worker may instead be exiting because its polling loop stopped.

### Changing `instances` in vercel.json does nothing

Check for a scaling override, which persists across deployments. Clear it with `vercel daemons scale orders-worker --reset`, replacing `orders-worker` with your daemon's name, or select **Clear scaling overrides** in the dashboard.

### Jobs run twice after a deploy

Check whether the same job was retried after losing its lock or stopping before completion, or whether two schedulers enqueued separate jobs. Make side effects idempotent and prevent overlapping schedulers from producing the same occurrence. If active jobs exceed the termination grace period, increase it within the supported limit or break the work into smaller jobs.

### The daemon didn't start on a preview deployment

That's expected during the beta. Only production deployments start daemons.

### The build fails

Check the build logs for these configuration problems:

- **No command to run.** Add `CMD` or `ENTRYPOINT` to the Dockerfile, or set `command`.
  
- **Colliding names.** Repeated hyphens collapse when Vercel names the image repository, so `a-b` and `a--b` clash. Rename one.
  
- **Output directory equals the daemon's** `root`**.** Keep worker code in its own directory, such as `worker/`.
  

## Frequently asked questions

### Do I still need the main Railway migration guide?

Yes, for everything around the worker. [How to migrate from Railway to Vercel](https://vercel.com/kb/guide/migrate-railway-to-vercel) covers the web app, databases, volumes, and DNS. It points continuous workers to Daemons; this guide walks through that move step by step.

### Should my Celery worker be a daemon or a Vercel Queues subscriber?

Use a daemon when your Celery deployment needs a continuously running worker and an external broker. Review its acknowledgment behavior, shutdown time, and any beat scheduler before migrating. [Celery on Vercel Queues](https://vercel.com/docs/frameworks/backend/celery) runs tasks as Functions and supports preview deployments; choose it when your tasks and configuration fit that integration's limits.

### Can a daemon call my app or API?

Yes. Daemons can make outbound connections to your app's production URL, external APIs, and databases, subject to those services' authentication and network rules. Daemons do not expose an inbound HTTP or TCP endpoint.

### How many workers can one project run?

You can define up to 100 daemons per project and configure up to 100 instances per daemon in a region. The project-environment limit of 50 running instances and your team's quota constrain the total you can run.

### Does a daemon waiting for work cost anything?

Hobby's monthly quota measures Active CPU usage. Waiting for work does not consume CPU at the same rate as processing jobs, but measure your worker's usage against that quota. On Pro and Enterprise, billing includes Active CPU and Provisioned Memory, so memory is billed while an instance runs even when its queue is empty.

## Related resources

- [The Complete Guide to Vercel Daemons](https://vercel.com/kb/guide/vercel-daemons) for a first daemon from scratch
  
- [How to migrate from Railway to Vercel](https://vercel.com/kb/guide/migrate-railway-to-vercel) for the web app, data, and DNS
  
- [Migrate a Python API from Railway to Vercel](https://vercel.com/kb/guide/migrate-a-python-api-from-railway-to-vercel) for FastAPI, Flask, Django, and cron routes
  
- [How to choose between Functions, containers, and Daemons on Vercel](https://vercel.com/kb/guide/vercel-functions-containers-daemons)
  
- [Vercel Queues poll mode](https://vercel.com/docs/queues/poll-mode), for a broker that lives on Vercel
  
- [Vercel Daemons is now in public beta](https://vercel.com/changelog/vercel-daemons-is-now-in-public-beta) on the changelog

## Related Documentation

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

## More Vercel Daemons guides

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