Skip to content
Docs

Run a Docker monolith with workers on Vercel

Run a Dockerized monolith on Vercel with Container Images, move long-running workers to Vercel Queues and Vercel Workflows, and keep previews for every PR.

Vercel

Vercel runs Docker container images as request-driven Vercel Functions on Fluid compute, so the HTTP surface of a Dockerized monolith deploys to Vercel while its always-on worker processes need a different shape. Container images on Vercel scale out with traffic and scale down when traffic stops, which means a worker loop that never receives an HTTP request has nothing keeping it running.

Most worker types have direct replacements on Vercel: queue consumers become Vercel Queues push consumers, multi-step jobs become Vercel Workflows, and scheduled tasks become Cron Jobs. Every pull request then gets a preview deployment that includes all services and their own isolated queue topics.

Processes that must run continuously can stay on Render, Railway, Amazon ECS, or Kubernetes and exchange work with Vercel through a shared queue such as Amazon SQS or Redis.

Copy link to headingOverview

In this guide, you'll learn how to:

  • Decide which processes in a Dockerized monolith fit Vercel and which need another home
  • Deploy the web and API process as a container image service
  • Replace queue consumers, multi-step jobs, and scheduled tasks with Vercel Queues, Vercel Workflows, and Cron Jobs
  • Keep preview deployments isolated for every pull request, including background jobs and data
  • Connect always-on workers on another platform in a hybrid architecture

Copy link to headingPrerequisites

Before you begin, make sure you have:

  • A Dockerized monolith in a Git repository connected to a Vercel project, or the Vercel CLI installed
  • A list of the monolith's process types, usually found in a Procfile, compose.yaml, supervisord.conf, or Kubernetes manifests
  • The Docker CLI and a running Docker daemon, which vercel dev needs to run container images locally
  • A plan for persistent state, such as a Postgres or Redis database from the Vercel Marketplace

Container images and Vercel Services are in beta and available on all plans. Vercel Queues is also in beta and available on all plans.

Copy link to headingCan a Dockerized monolith run on Vercel?

A Dockerized monolith runs on Vercel when its runtime entrypoint is an HTTP server. Vercel builds a Dockerfile.vercel (or Containerfile.vercel) into an OCI image, stores it in Vercel Container Registry, and runs it as a Vercel Function that scales with traffic. Each container instance is stateless, so persistent state has to live in backing services such as a Marketplace database or Vercel Blob. Worker processes that never receive an HTTP request don't run on Vercel as-is. You decompose them into Vercel Queues consumers, Vercel Workflows, or Cron Jobs, or you keep them on a platform built for always-on processes.

Copy link to headingHow Vercel runs container images

Vercel treats a container image as a Vercel Function with the same limits and Active CPU pricing as any other function. The behavior that matters for a monolith is:

  • HTTP only: The container must open an HTTP server. Vercel routes traffic to port 80 by default, and you can override it by setting the PORT environment variable in your project settings.
  • Scale to zero: Instances with no traffic for 5 minutes in production scale down automatically. In preview environments, the threshold is 30s.
  • Graceful shutdown: On scale-down, the container receives a SIGTERM signal with a 30s grace period before Vercel terminates it.
  • Stateless instances: Nothing written to the local filesystem or process memory survives between instances.
  • Duration limits: Each request is bound by the function's maxDuration, which defaults to 300s and goes up to 800s on Pro and Enterprise. The 1800s extended maximum beta only covers specific Node.js, Bun, and Python runtime versions, and container images aren't on that list, so plan for 800s.
  • Networking gaps: Secure Compute and Static IPs aren't supported with container images yet.

Copy link to headingWhy an always-on worker loop doesn't fit container images

A typical worker command, such as celery worker, sidekiq, or a BullMQ Worker, opens a connection to a broker and loops forever waiting for jobs. That process never serves an HTTP request, so Vercel has no request to route to it and no traffic to keep the instance alive. A worker started as a background loop inside the web container also stops working, because the instance scales down after 5 minutes without HTTP traffic in production and 30s in preview. Vercel inverts the model instead. Vercel Queues holds the work and invokes a private consumer function for each message, so compute only runs when there's a job to process.

Copy link to headingWhich Vercel runtime each monolith process maps to

Map every process type in your monolith to one row of this table before changing any code. The worker type, not the fact that it's a worker, decides where it runs.

Monolith processExample start commandWhere it runsVercel primitive
Web server or APInode src/server.js, gunicorn app.wsgi, rails serverVercelContainer image in a Vercel Services project
Fire-and-forget jobs (emails, PDFs, webhooks)celery worker, sidekiq, BullMQ WorkerVercelVercel Queues push consumer
Multi-step or long-waiting jobs (onboarding, billing runs, approvals)Chained jobs with manual state trackingVercelVercel Workflows
Scheduled taskscron, celery beat, wheneverVercelCron Jobs that enqueue work or start a workflow
Always-on daemons (persistent broker or socket connections, local disk, privileged host access)Custom daemon or sidecarAnother platformRender, Railway, Amazon ECS, or Kubernetes, connected through a shared queue such as Amazon SQS or Redis

Vercel Queues and Vercel Workflows solve different problems. Vercel Queues gives you direct control over topics, messages, and consumer groups, which fits one-message-one-job work. Vercel Workflows builds on Vercel Queues and adds durable steps, sleep, and hooks, so a job can run across many function invocations and pause for minutes to months without a duration limit.

Copy link to headingSteps

The steps below use a Node.js Express monolith with a web process, a job worker, and a scheduler as the running example. The same decomposition applies to Python, Ruby, Go, and Java monoliths. Python projects can use the Celery and Dramatiq integrations, which compile existing workers into private queue-triggered functions. These integrations run on the Python runtime and declare their workers in pyproject.toml, so put the worker code in a Python service next to the container image rather than inside it. Service bindings aren't available yet for services on the Go or Rust runtime, so build a Go service as a container image if it needs to call another service.

Copy link to heading1. Inventory the monolith's process types

List every process the monolith starts in production. A Procfile for the example monolith looks like this:

web: node src/server.js
worker: node src/worker.js
scheduler: node src/scheduler.js

For each process, record what triggers it, how long one unit of work takes, and whether it depends on local disk or a persistent connection. Then assign it a row from the mapping table above. In the example, web becomes a container image service, worker becomes a Vercel Queues consumer, and scheduler becomes a Cron Job that starts a Vercel Workflow.

A unit of work that regularly runs longer than your function's maxDuration belongs in Vercel Workflows, where you split it into steps that each finish within the limit.

Copy link to heading2. Move durable state out of the container

Container instances on Vercel keep nothing between requests, so every piece of state the monolith writes locally needs a backing service. Replace each local dependency before you deploy:

  • Database containers: Attach a managed database from the Vercel Marketplace, which injects its connection string as an environment variable. For example, run vercel install neon for Postgres or vercel install upstash for Redis.
  • Uploaded or generated files: Move them to Vercel Blob instead of a mounted volume.
  • Sessions and caches: Store them in Redis or your database instead of process memory.
  • Job progress: Persist it in the database so a restarted consumer can resume or skip completed work.

Your application code keeps its existing Postgres or Redis client and reads the connection string from the injected environment variable.

Copy link to heading3. Deploy the web and API process as a container image

Move the monolith into a web/ directory and add a Dockerfile.vercel next to its package.json. The image only needs to start the HTTP server:

web/Dockerfile.vercel
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "src/server.js"]

The server listens on PORT when you set it and falls back to Vercel's default port 80. It also closes cleanly on SIGTERM so in-flight requests finish during the 30s grace period:

web/src/server.js
import express from 'express';
const app = express();
app.use(express.json());
app.get('/healthz', (req, res) => res.json({ ok: true }));
const port = Number(process.env.PORT ?? 80);
const server = app.listen(port, '0.0.0.0', () => {
console.log(`Listening on port ${port}`);
});
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
// Exit before the 30s grace period ends if connections don't drain
setTimeout(() => process.exit(1), 25_000).unref();
});

Remove any code that starts the worker loop or scheduler from inside the web process. Those processes move to the jobs service in the next steps.

Copy link to heading4. Add a jobs service and route traffic between services

Create a jobs/ directory with a Next.js app that will hold queue consumers and workflows. The Vercel Queues docs configure push consumers on framework function paths, such as a Next.js route handler, so the consumers live in a framework service next to the container image.

Declare both services in a vercel.json at the repository root:

{
"$schema": "https://openapi.vercel.sh/vercel.json",
"services": {
"web": {
"root": "web/",
"runtime": "container",
"entrypoint": "Dockerfile.vercel",
"bindings": [
{
"type": "service",
"service": "jobs",
"format": "url",
"env": "JOBS_INTERNAL_URL"
}
]
},
"jobs": {
"root": "jobs/",
"functions": {
"app/api/queues/send-invoice/route.ts": {
"experimentalTriggers": [
{ "type": "queue/v2beta", "topic": "invoices", "maxDeliveries": 10 }
]
}
}
}
},
"rewrites": [
{ "source": "/(.*)", "destination": { "service": "web" } }
],
"crons": [
{ "path": "/api/cron/nightly-reconcile", "schedule": "0 3 * * *" }
]
}

This configuration does four things:

  • Public traffic: The rewrite sends every public request to the web container. The jobs service has no rewrite, so it receives no public traffic.
  • Internal calls: The binding on web injects the jobs service's internal URL as JOBS_INTERNAL_URL. Traffic over a binding stays on Vercel's internal network, and the injected URL is deployment-aware, so a preview's web always calls the same preview's jobs. A binding grants reachability but doesn't authenticate the caller, so keep authorization between services in your code if it matters.
  • Queue consumer: The queue/v2beta trigger makes send-invoice/route.ts a private consumer of the invoices topic. maxDeliveries caps retries for a message that keeps failing.
  • Schedule: The cron entry calls /api/cron/nightly-reconcile on the production deployment at 03:00 UTC every day. The cron request is an HTTP GET to that path on the production URL, which the catch-all rewrite sends to web, where step 6 adds the handler. Step 6 shows how to confirm this on your first production deployment. Vercel doesn't retry a failed cron invocation, which is another reason to keep the cron handler thin.

Enable Vercel Workflows in the jobs service by installing workflow and @vercel/queue, then wrapping the Next.js config:

npm i workflow @vercel/queue
jobs/next.config.ts
import { withWorkflow } from 'workflow/next';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {};
export default withWorkflow(nextConfig);

Without withWorkflow(), the 'use workflow' and 'use step' directives compile to no-ops.

Copy link to heading5. Convert the first queue consumer

Pick one low-risk job, such as sending an invoice email, and move it off the old worker. The web process publishes work through the jobs service, and Vercel Queues invokes the consumer for each message.

Add an internal route in the jobs service that publishes to the invoices topic. The idempotencyKey drops duplicate publishes for the same invoice during the message's retention period:

jobs/app/api/enqueue/invoice/route.ts
import { send } from '@vercel/queue';
export async function POST(request: Request) {
const { invoiceId } = await request.json();
const { messageId } = await send(
'invoices',
{ invoiceId },
{ idempotencyKey: `invoice-${invoiceId}` },
);
return Response.json({ messageId });
}

Add the consumer. Vercel acknowledges the message when the handler returns and redelivers it when the handler throws:

jobs/app/api/queues/send-invoice/route.ts
import { handleCallback } from '@vercel/queue';
import { sendInvoice } from '@/lib/invoices';
export const POST = handleCallback(
async (message: { invoiceId: string }) => {
// sendInvoice must be idempotent: check a "sent" flag in the database
// before sending, because Vercel Queues delivers at least once
await sendInvoice(message.invoiceId);
},
{
retry: (error, metadata) => {
if (metadata.deliveryCount >= 5) {
return { acknowledge: true };
}
return { afterSeconds: Math.min(300, 2 ** metadata.deliveryCount * 5) };
},
},
);

The retry callback drops a message after 5 failed deliveries. The trigger's maxDeliveries: 10 from step 4 is a backstop for failures the callback never sees, such as a function timeout or crash, where the handler doesn't get to run its retry logic.

Replace the old enqueue call in the web process with a call to the jobs service over the binding:

web/src/server.js
app.post('/api/invoices/:id/send', async (req, res) => {
const response = await fetch(
new URL('api/enqueue/invoice', process.env.JOBS_INTERNAL_URL),
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ invoiceId: req.params.id }),
},
);
if (!response.ok) {
return res.status(502).json({ error: 'enqueue_failed' });
}
res.status(202).json(await response.json());
});

Deploy and send a request to the new endpoint:

curl -X POST https://your-preview-url.vercel.app/api/invoices/inv_123/send

The response includes a messageId, and the jobs service's runtime logs show the consumer processing the message. Publishing through the jobs service keeps every Vercel Queues call in a framework service, where the SDK's credential handling is documented.

Copy link to heading6. Convert scheduled and multi-step jobs

Scheduled jobs work best as thin triggers that start durable work and return. A cron request is bound by the function's maxDuration, while a workflow can run across many invocations with no duration limit.

Define the nightly reconciliation as a workflow in the jobs service. Each 'use step' function is persisted, so a failure retries only the failed step:

jobs/workflows/nightly-reconcile.ts
import { listAccountsToReconcile, reconcileAccount } from '@/lib/billing';
export async function nightlyReconcile(date: string) {
'use workflow';
const accountIds = await loadAccounts(date);
for (const accountId of accountIds) {
await reconcile(accountId, date);
}
return { date, reconciled: accountIds.length };
}
async function loadAccounts(date: string) {
'use step';
// Returns every account with outstanding work since the last successful run,
// so a missed night catches up on the next run
return listAccountsToReconcile(date);
}
async function reconcile(accountId: string, date: string) {
'use step';
// Skips accounts already reconciled for this date, so duplicate runs are safe
await reconcileAccount(accountId, date);
}

Add an internal route that starts a run:

jobs/app/api/workflows/nightly-reconcile/route.ts
import { start } from 'workflow/api';
import { nightlyReconcile } from '@/workflows/nightly-reconcile';
export async function POST(request: Request) {
const { date } = await request.json();
const run = await start(nightlyReconcile, [date]);
return Response.json({ runId: run.runId });
}

Add the cron route to the web container. It checks the CRON_SECRET environment variable, starts the workflow over the binding, and returns within seconds:

web/src/server.js
app.get('/api/cron/nightly-reconcile', async (req, res) => {
const cronSecret = process.env.CRON_SECRET;
if (!cronSecret || req.get('authorization') !== `Bearer ${cronSecret}`) {
return res.status(401).end();
}
const date = new Date().toISOString().slice(0, 10);
const response = await fetch(
new URL('api/workflows/nightly-reconcile', process.env.JOBS_INTERNAL_URL),
{
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ date }),
},
);
if (!response.ok) {
return res.status(502).json({ error: 'workflow_start_failed' });
}
res.json(await response.json());
});

Set CRON_SECRET to a random string of at least 16 characters in every environment that serves this route, including Preview. The handler rejects all requests when the variable is missing, and a failed workflow start returns a non-2xx status so it shows up in the cron logs.

Cron delivery is best effort. A scheduled run can occasionally be skipped or invoked more than once, so make the work safe to repeat. reconcileAccount should check whether an account was already reconciled for date before doing anything, and listAccountsToReconcile should return all outstanding work since the last successful run so a missed night catches up the next day.

After the next production deployment, trigger the cron job manually to confirm it reaches the web container instead of waiting for 03:00 UTC:

vercel crons run /api/cron/nightly-reconcile

vercel crons is in beta. It reads cron definitions from your deployed production project, so the cron entry must already be in production. Check the result under Settings, then Cron Jobs, then View Logs, and open your project's Observability tab and select Workflows to see each run, its steps, and any retries.

On the Hobby plan, cron jobs can run at most once per day, and Vercel may invoke them at any point within the scheduled hour, so the 0 3 * * * schedule can run anytime between 03:00 and 03:59 UTC. On other plans, jobs run within the specified minute.

Copy link to heading7. Validate the preview deployment

Open the pull request's preview deployment and confirm each part of the monolith works before you retire the old workers:

  • The web container responds at the preview URL, including /healthz
  • Publishing a message from the preview produces a consumer log in the same preview deployment
  • Calling the cron route manually with the preview's CRON_SECRET starts a workflow run
  • The preview reads and writes only preview data, never production data

Run vercel dev from the repository root to run every service locally with the same routing and bindings. Cron schedules don't fire locally, so call /api/cron/nightly-reconcile directly with your local CRON_SECRET to test the scheduled path.

Copy link to headingHow preview deployments isolate web, jobs, and data

Every commit to a pull request creates a preview deployment with its own URL, and every service in the project deploys atomically on that deployment's shared domain. A preview of the example monolith therefore includes the web container, the jobs service, and its queue consumers at the same commit. Services also roll back together, so an Instant Rollback returns web and jobs to a matching version. Cron jobs are the exception. An Instant Rollback doesn't update active cron jobs, which keep running on their current schedule until you disable or update them.

Copy link to headingQueue topics are isolated per deployment

Vercel Queues partitions topics by deployment ID by default, and in push mode it delivers messages back to the same deployment that published them. A preview deployment's consumers only process messages that the preview published, so preview jobs never consume production messages and schema changes in a pull request never break production consumers. You get a preview worker for every pull request without provisioning anything.

Vercel Workflows runs on Vercel Queues and keeps each run on the deployment that created it by default, so a workflow run started from a preview executes against that preview's code, and a new production deployment doesn't change runs already in progress.

Copy link to headingCron Jobs only run in production

Vercel triggers cron jobs by sending an HTTP GET request to the production deployment URL, so schedules never fire on preview deployments. To test scheduled work in a preview, call the cron route yourself:

curl -H "Authorization: Bearer your_preview_cron_secret_here" \
-H "x-vercel-protection-bypass: your_automation_bypass_secret_here" \
https://your-preview-url.vercel.app/api/cron/nightly-reconcile

The x-vercel-protection-bypass header is only needed when Deployment Protection covers preview URLs. Create the secret under Protection Bypass for Automation in your project's Deployment Protection settings. Calls over service bindings skip the public request pipeline that Deployment Protection runs in, and queue consumers are invoked by Vercel's internal queue infrastructure rather than through a public URL, so neither needs the bypass header.

Copy link to headingPreview data needs its own database or branch

Preview deployments share whatever database their environment variables point to. Set the Preview environment's variables to a separate database, or use a Marketplace provider that branches the database per preview deployment. The Neon integration creates a database branch named preview/<git-branch> when you enable Create Database Branch for Deployment for Preview. Every preview deployment of that Git branch shares the same database branch, so data written by one commit's preview is still there for the next. Neon injects the branch's connection string into each preview deployment at deploy time. It overrides the Preview environment variables for that deployment and doesn't appear in your project's environment variable settings. To keep the schema in step with the code, run your migrations in the build command, for example npx prisma migrate deploy && npm run build. Without that separation, a preview's jobs write to the same tables as production.

Copy link to headingStale previews can keep processing messages

Messages stay pinned to the deployment that published them, so an old preview keeps receiving retries until its messages are acknowledged or expire, which takes 24 hours by default. Delete preview deployments you no longer need to stop their consumers immediately.

Copy link to headingRun always-on workers on another platform

Some processes don't decompose into queue messages or workflow steps. Keep those workers on a platform that runs always-on containers and connect them to the Vercel-hosted web tier through a shared queue or database.

Copy link to headingWorkers that should stay always-on

Keep a worker on Render, Railway, Amazon ECS, or Kubernetes when it:

  • Holds a persistent connection it can't give up, such as a stream consumer or a long-lived socket to a third-party system
  • Depends on local persistent disk or large in-memory state that's expensive to rebuild
  • Needs privileged host access, custom kernels, or host networking
  • Needs a fixed outbound IP address for allowlisting, since container images don't support Static IPs yet
  • Runs a single unit of work that can't be split into steps and exceeds your function's maxDuration

Copy link to headingConnect external workers through a shared queue

An external worker needs a queue that both platforms can authenticate to. The web tier on Vercel publishes jobs, and the worker consumes them with the client and credentials it already uses on its own platform. Amazon SQS works well for this, because code running on Vercel, including container images, can assume an IAM role over OpenID Connect (OIDC) federation instead of storing AWS access keys.

Publish from the web container with the AWS SDK and the Vercel OIDC credentials provider:

npm i @aws-sdk/client-sqs @vercel/oidc-aws-credentials-provider
web/src/transcode-queue.js
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
const sqs = new SQSClient({
region: process.env.AWS_REGION,
credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN }),
});
export async function enqueueTranscode(job) {
await sqs.send(
new SendMessageCommand({
QueueUrl: process.env.TRANSCODE_QUEUE_URL,
MessageBody: JSON.stringify(job),
}),
);
}

The worker on Amazon ECS, Kubernetes, Render, or Railway keeps its existing SQS consumer. Only the producer moves to Vercel.

Setting this up takes three pieces of configuration:

  • OIDC identity provider: Register Vercel as an OpenID Connect provider in AWS IAM, then create a role whose trust policy admits your Vercel project. The OIDC federation docs and Migrate self-hosted Next.js and containers from AWS to Vercel walk through the trust policy.
  • Pinned region: Set AWS_REGION explicitly in your project's environment variables. Vercel sets AWS_REGION to the region the function runs in, so an unpinned client can address a queue in a region where it doesn't exist.
  • Per-environment values: Set AWS_ROLE_ARN and TRANSCODE_QUEUE_URL separately for the Production and Preview environments, as described in the next section.

Vercel Queues also supports consumers outside Vercel through poll mode, which is designed for long-running services, on-premise workers, and other clouds. Python projects using Celery can point an external celery worker at the same queues with the vercel-poll:// broker. Every Vercel Queues API request requires a Vercel OIDC token in the Authorization header, so confirm how your external worker will obtain and refresh that token before choosing this path. If that isn't settled, a broker your worker already authenticates to, such as Amazon SQS or Redis, is the lower-risk choice.

Copy link to headingKeep your existing queue and move only the web tier

The lowest-change hybrid keeps the monolith's existing queue library. The web container on Vercel enqueues jobs into your existing broker, such as Redis from the Vercel Marketplace or a broker you already run, and the unchanged worker process keeps running on its current platform. Use this path to move the web tier first and convert workers later, one job type at a time.

Copy link to headingPreview workers in a hybrid setup

External workers don't get the per-deployment isolation that Vercel Queues provides, so separate preview traffic yourself. Point the Preview environment's TRANSCODE_QUEUE_URL at a preview queue, and give previews their own IAM role. Scope each role's trust policy to one project and one environment, such as owner:[TEAM_SLUG]:project:[PROJECT_NAME]:environment:preview, so preview deployments can never publish to the production queue. Avoid a wildcard that matches environment:preview across every project on your team, because anyone who can push a branch to any of those projects could then assume the role.

Then choose how preview jobs get processed:

  • Shared preview worker: Run one external worker that consumes the preview queue for all pull requests. It's the lowest-cost option, but every preview shares one worker and one set of preview data.
  • Per-pull-request worker: Create a queue and worker per pull request on the external platform. Tear both down when the pull request closes, because the external platform won't clean them up when Vercel removes the preview.

Copy link to headingChoose a Vercel-only, hybrid, or container-platform architecture

Use the process inventory from step 1 to pick the architecture. The deciding question is whether any process must stay running when there's no work to do.

ArchitectureFits whenPreviews for every pull request
Vercel-onlyEvery process is an HTTP server, a queue consumer, a multi-step job, or a scheduled taskAutomatic for web, API, and queue consumers, with per-deployment topic isolation
HybridWeb and API fit Vercel, but one or more workers need an always-on processAutomatic for Vercel services; external workers need a shared preview worker or per-pull-request provisioning
Container platformThe monolith must stay one image with private services, attached disks, and always-on workers cloned into every previewHandled by the container platform's preview environments

Many teams start hybrid, moving the web tier and the easiest job types to Vercel first, then converting the remaining workers as they're rewritten. A container platform remains the better fit when most of the monolith's value sits in stateful, always-on processes. For a feature-by-feature comparison of the compute models, see Running Docker on Vercel vs Render.

Copy link to headingTroubleshooting

Copy link to headingThe deployment ignores the services configuration

Vercel switches a project into services mode when vercel.json defines a services key. Confirm that vercel.json sits at the repository root and contains the services block. In services mode, build and runtime keys such as functions, buildCommand, and framework aren't valid at the top level, so move them into the relevant service, then redeploy.

Copy link to headingThe deployment builds but requests fail or time out

The container isn't listening where Vercel sends traffic. Bind the server to 0.0.0.0, not localhost, and listen on PORT with a fallback to 80. Confirm the startup command in Dockerfile.vercel starts the HTTP server rather than a worker.

Copy link to headingBackground work stops after a few minutes

A worker loop started inside the web container dies when the instance scales down, which happens after 5 minutes without traffic in production and 30s in preview. Move that loop to a Vercel Queues consumer, a Vercel Workflow, or an external worker.

Copy link to headingFiles or cached values disappear between requests

Container instances are stateless, and each request can land on a different instance. Move files to Vercel Blob and caches or sessions to Redis or your database.

Copy link to headingA job runs more than once

Vercel Queues delivers at least once, so a message can arrive again after a consumer timeout or an infrastructure event. Make handlers idempotent by recording completed work in the database, and use idempotencyKey when publishing to drop duplicate publishes. Cron Jobs can also invoke the same scheduled run more than once, so apply the same idempotency to work started from a cron route.

Copy link to headingAn old deployment keeps running consumers after a rollback

Messages stay pinned to the deployment that published them, so promoting or rolling back doesn't stop the old deployment's consumers. Delete the old deployment to stop its consumers immediately, and set maxDeliveries on triggers to bound retries. The Observability Query tab can show function duration grouped by deployment to find which deployment is consuming compute.

Copy link to headingA request returns a 504 FUNCTION_INVOCATION_TIMEOUT error

The request ran longer than the function's maxDuration. Return quickly from HTTP routes and move the work to a queue consumer, or split it into Vercel Workflow steps that each finish within the limit.

Copy link to headingNext steps

More Vercel Services guides