---
title: Migrate a Python API from Railway to Vercel
description: Move a FastAPI, Flask, or Django API from Railway to Vercel Functions. Migrate configuration, data, workers, and scheduled jobs.
url: "https://vercel.com/kb/guide/migrate-a-python-api-from-railway-to-vercel"
published: 2026-10-14
last_updated: 2026-10-14
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

You can move a FastAPI, Flask, or Django API from Railway to [Vercel's native Python runtime](https://vercel.com/docs/functions/runtimes/python) without a Dockerfile when its dependencies fit the supported environment. Vercel installs the application's Python packages and loads its entrypoint, so the server start command configured in Railpack or `railway.json` no longer controls how the API runs.

The rest of the migration depends on what the Railway project provides around that API. Files stored on volumes need external storage, and Railway reference variables need to resolve to the services the Vercel deployment will use. Workers and scheduled jobs need their own deployment configuration, while database migrations move out of startup and into a release pipeline.

## Overview

By the end, you'll have:

- Pinned the Python version Railpack resolved in a file Vercel reads.
  
- Run your API on Vercel's native Python runtime without `deploy.startCommand` or `PORT`.
  
- Replaced Railway volumes, Postgres, and Redis with external services and resolved Railway reference variables.
  
- Replaced Celery worker services with Vercel Queues subscribers and Railway cron services with Vercel Cron Jobs.
  
- Moved pre-deploy and startup migrations to CI and production traffic to Vercel.
  

## Prerequisites

- Access to the API's repository and Railway project.
  
- A Vercel account and the latest [Vercel CLI](https://vercel.com/docs/cli) installed.
  
- Python and [uv](https://docs.astral.sh/uv/getting-started/installation/) installed locally.
  
- The [Railway CLI](https://docs.railway.com/cli), installed and authenticated.
  
- [PostgreSQL client tools](https://www.postgresql.org/download/) (`pg_dump`, `pg_restore`, and `psql`) for database transfers.
  

## Should your API move to Vercel?

Check that your API and its dependencies fit the native runtime:

- Can sessions, uploads, and other persistent state live outside the API process?
  
- Do dependencies install from Python packages and compatible wheels, or does the application need system packages?
  
- Does background work run as bounded queue tasks, or does a process need to stay running indefinitely?
  
- Do requests fit the function's duration and payload limits?
  

[Railpack can install system dependencies](https://railpack.com/languages/python) for packages such as `pydub` and `pdf2image`. Audit those dependencies before choosing the native runtime. If the application needs FFmpeg, Poppler, or other system packages, use the [Python container deployment path](https://vercel.com/kb/guide/vercel-docker-python-apps), which uses Container Images in Beta.

For an API that uses WebSockets, [Python WebSocket support is in Beta](https://vercel.com/docs/functions/websockets). Connections follow function duration limits, so clients need a reconnection strategy.

With Fluid compute, the default `maxDuration` is 300s. Hobby allows up to 300s; Pro and Enterprise allow up to 800s, with an opt-in extended-duration Beta up to 1,800s. Request and response bodies have a 4.5 MB limit. Check the [function limits](https://vercel.com/docs/functions/limitations) against your routes before cutover.

## How Railway concepts map to Vercel

| Railway                                    | Vercel                                                            |
| ------------------------------------------ | ----------------------------------------------------------------- |
| Railpack Python build                      | Framework detection and Python dependency installation            |
| `RAILPACK_PYTHON_VERSION` or `runtime.txt` | A supported repository version file, such as `.python-version`    |
| `deploy.startCommand` and `PORT`           | Native Python application entrypoint                              |
| Custom build command                       | Project Build Command or `tool.vercel.scripts.build`              |
| Service variables and reference variables  | Project environment variables and connected-service credentials   |
| Postgres and Redis services                | External databases, including providers in the Vercel Marketplace |
| Volume                                     | Object storage or an external database                            |
| Celery worker service                      | A subscriber on Vercel Queues, in Beta                            |
| `deploy.cronSchedule`                      | A cron route configured in `vercel.json`                          |
| `deploy.preDeployCommand`                  | A migration step in continuous integration                        |
| `deploy.healthcheckPath`                   | Deployment checks against the preview URL                         |
| PR environments                            | Preview deployments                                               |

## Steps

Create a migration branch and keep the Railway deployment running until cutover. Skip the database, worker, or cron steps your API doesn't need.

### 1\. Pin a supported Python version and dependencies

Read the running Python version from your Railway deployment or build logs. [Railpack version selection](https://railpack.com/languages/python) checks `RAILPACK_PYTHON_VERSION` before repository version files. Copy the resolved version into a file Vercel reads; a Railway-only environment variable is not a repository pin.

Vercel supports [Python 3.12, 3.13, and 3.14](https://vercel.com/docs/functions/runtimes/python/python-version), with 3.12 as the default. If the application runs an older version, test the upgrade before migration.

For an application tested on Python 3.13, commit a `.python-version` file at the Vercel project root that contains the major and minor version:

```text
3.13
```

Keep the `requires-python` constraint in `pyproject.toml` aligned with your Python version pin. Since an open lower bound such as `>=3.12` also permits 3.14, use a bounded constraint for Python 3.13:

```toml
[project]
name = "my-api"
version = "0.1.0"
requires-python = "~=3.13.0"
dependencies = [
    "fastapi>=0.117.1",
    "sqlalchemy>=2,<3",
    "psycopg[binary]>=3,<4",
    "redis>=5",
    "celery>=5.3,<6",
    "alembic>=1.13,<2",
]
```

Merge the relevant entries into your existing manifest, retaining the application's other dependencies. Keep your existing dependency workflow if it uses a manifest Vercel supports. With uv, generate the lockfile and install its dependencies:

```bash
uv lock
uv sync
```

The commands create or update `uv.lock` and install the resolved packages into `.venv`. Test with that environment, then commit `uv.lock` with the manifest changes. Vercel also accepts `requirements.txt` and Pipenv manifests; [Celery on Vercel](https://vercel.com/docs/frameworks/backend/celery) requires `pyproject.toml`.

For a Poetry project, convert the dependencies to the `[project]` format or export a `requirements.txt` using [Poetry's export plugin](https://github.com/python-poetry/poetry-plugin-export). Don't assume `poetry.lock` alone determines the Vercel build. Check native dependencies against the chosen Python version, especially older `psycopg2-binary` pins without matching wheels.

Python function bundles have a 500 MB uncompressed limit. Exclude tests, local environments, and other unneeded files with [`excludeFiles`](https://vercel.com/docs/functions/runtimes/python); Python builds don't automatically tree-shake the application.

[Large Functions](https://vercel.com/docs/functions/limitations), in Beta, raise the limit to 5 GB on projects using Fluid compute and Active CPU pricing. Check the opt-in requirements for an existing project; Large Functions don't support Secure Compute or Static IPs.

### 2\. Configure the Python entrypoint

Railway can start a FastAPI service with this setting in `railway.json`:

```json
{
  "$schema": "https://railway.com/railway.schema.json",
  "deploy": {
    "startCommand": "uvicorn main:app --host 0.0.0.0 --port $PORT"
  }
}
```

The `main:app` target already identifies the file and object Vercel needs. Move any setup chained before that command into the appropriate migration or build step.

Vercel loads the application object from a supported entrypoint, such as `app` in a FastAPI application's `main.py`. Add this health check alongside your existing routes:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}
```

For other entrypoint locations, set the module path in `pyproject.toml`:

```toml
[tool.vercel]
entrypoint = "my_api.server:app"
```

Use `my_api.server:app` to load `app` from `my_api/server.py`, or `main:app` for the root-level example. [Flask](https://vercel.com/docs/frameworks/backend/flask) also exports `app`; [Django](https://vercel.com/docs/frameworks/full-stack/django) uses its configured WSGI or ASGI application. Set Django's allowed hosts for the deployment and follow its framework instructions for static files.

Keep Gunicorn or Uvicorn if another environment uses them. The native Vercel deployment doesn't use Railway's server start command or require a `PORT` setting. Keep source-platform configuration until the source deployment is retired.

### 3\. Replace writes to Railway volumes

Inventory every path the application writes under a Railway volume mount. Copy existing files to the new storage destination and update both reads and writes before testing the migrated API. An empty destination doesn't preserve uploads already stored on Railway.

Move uploaded files and generated artifacts to persistent object storage such as [Vercel Blob](https://vercel.com/docs/vercel-blob). Put sessions and durable application records in Redis or a database, according to their persistence requirements. Replace SQLite files that the running application modifies with an external database.

The function filesystem is read-only except for [up to 500 MB of scratch space in](https://vercel.com/docs/functions/runtimes) [`/tmp`](https://vercel.com/docs/functions/runtimes). Treat that directory as temporary and local to an instance. For uploads above the function body limit, use a [direct client upload](https://vercel.com/docs/vercel-blob/client-upload) flow so the file doesn't pass through the Python API.

### 4\. Provision backing services and environment variables

#### Link the Vercel project

From the repository's application directory, link a new Vercel project:

```bash
vercel link
```

Choose the intended team and project when prompted. Commercial APIs require Pro or Enterprise; [Hobby is limited to non-commercial personal use](https://vercel.com/docs/limits/fair-use-guidelines#commercial-usage).

The CLI saves project metadata in `.vercel`, which should stay out of version control along with exported secrets and database dumps. Add the following entries to `.gitignore` before exporting configuration or pulling local variables:

```text
.vercel/
.venv/
.env*
!.env.example
*.dump
```

#### Review Railway variables

Run `railway link` to select the source project. Export variables for the intended service and environment using the [Railway CLI](https://docs.railway.com/cli/variable):

```bash
umask 077
railway variable list --service your_service_name_here \
  --environment your_environment_name_here --kv > .env.railway
```

Review the exported `KEY=VALUE` entries locally before transferring them to Vercel. Railway sealed variables are omitted from CLI exports; obtain or reissue those credentials through the system that owns them.

Replace reference expressions such as `${{Postgres.DATABASE_URL}}` with the destination provider's values; copying the expression itself won't connect a Vercel project to that service.

#### Add PostgreSQL with Neon

[Neon in the Vercel Marketplace](https://vercel.com/marketplace/neon) connects managed PostgreSQL databases to your Vercel project. From the linked application directory, run [`vercel integration add`](https://vercel.com/docs/cli/integration#vercel-integration-add) to create separate databases for Development, Preview, and Production:

```bash
vercel integration add neon --name api-development \
  --environment development --no-env-pull
vercel integration add neon --name api-preview \
  --environment preview --no-env-pull
vercel integration add neon --name api-production \
  --environment production --no-env-pull
```

The CLI installs the integration if needed, prompts for resource settings and a billing plan, and connects each database to the linked project. Scoping each resource with `--environment` keeps development and preview test writes separate from production. The `--no-env-pull` flag skips the automatic variable download; the local testing step pulls Development values when you need them.

Choose a destination PostgreSQL version compatible with the source database and enable any required extensions before restoring data. Place the database near the API's [function region](https://vercel.com/docs/functions/configuring-functions/region) to limit network latency between them.

The integration supplies connection strings for different uses:

| Environment variable    | Use                                                            |
| ----------------------- | -------------------------------------------------------------- |
| `DATABASE_URL`          | Pooled connection for application queries                      |
| `DATABASE_URL_UNPOOLED` | Direct connection for exports, restores, and schema migrations |

If you selected an integration prefix, use the resulting variable names in your application and migration commands. Leave [Neon preview branching](https://neon.com/docs/guides/vercel-managed-integration) disabled for this rehearsal so the Preview deployment uses the database you restore in step 5.

If you enable branching later, each deployment needs its own branch migrations. Its injected connection strings aren't available through `vercel env pull`.

#### Connect Redis if the API uses it

Connect a Redis provider from the [Vercel Marketplace](https://vercel.com/marketplace) and obtain its TCP connection URL for your Python client. TLS connections typically use `rediss://`; REST credentials such as `KV_REST_API_URL` require a different client interface. Set `REDIS_URL` to the TCP URL and follow the provider's TLS verification instructions.

Treat Redis cache entries separately from sessions, queued messages, or durable application data. Only discard data that the application can rebuild.

For state you need to keep, rehearse a transfer supported by the destination provider and plan a final synchronization while Redis writers are paused. If sessions will be invalidated instead, make the resulting sign-in requirement part of the cutover plan. Handle queued messages through the worker handoff in step 12.

#### Add the remaining application secrets

Once the destination services are connected, open the Vercel project's Settings, then Environment Variables, and add the application secrets you retained from Railway. Select the appropriate Development, Preview, and Production scopes for each value. Review entries individually so imported Railway credentials don't overwrite integration-provided connection strings.

[Vercel's 64 KB limit](https://vercel.com/docs/environment-variables#environment-variable-size) applies to all environment-variable data in a deployment. Large values such as certificates or service-account files can exceed it, so check their size before adding them.

### 5\. Rehearse the database transfer

For Postgres, confirm that the export role can read all required data and that the destination supports the application's extensions.

Use `pg_dump` and `pg_restore` from the same client-tool release. Its major version must be at least the source server's version and no newer than the destination's. Matching all three major versions avoids a cross-version transfer.

Restore into the empty Preview database so Production stays empty for the final transfer. Set `SOURCE_DATABASE_URL` and `DESTINATION_DATABASE_URL` in your shell to direct PostgreSQL connection URLs. Keep SQLAlchemy-specific driver prefixes out of URLs passed to PostgreSQL command-line tools.

With preview branching disabled as in step 4, get the destination's `DATABASE_URL_UNPOOLED` from the Neon console, or copy it from `.env.preview` after running `vercel env pull .env.preview --environment=preview`.

If you enable preview branching instead, create the deployment in step 11 first, then identify its branch in Neon and repeat the restore and migrations against that branch's direct URL before testing. Deployment-specific connection strings are unavailable through `vercel env pull`.

Enable the Railway database's TCP Proxy and set `SOURCE_DATABASE_URL` to its public `DATABASE_PUBLIC_URL`. Follow the [provider's migration procedure](https://neon.com/docs/import/migrate-from-railway) to export through that direct connection:

```bash
umask 077
pg_dump --format=custom --no-owner --no-acl \
  --file=migration.dump "$SOURCE_DATABASE_URL"
```

The command creates `migration.dump` when it succeeds. Don't use a pooled connection for the export.

```bash
pg_restore --exit-on-error --no-owner --no-acl \
  --dbname="$DESTINATION_DATABASE_URL" migration.dump
```

Continue only after the restore exits with status 0. `--exit-on-error` stops at the first error without undoing earlier changes; investigate failures and retry only on a clean, disposable destination. Apply the application's required privileges separately because `--no-owner` and `--no-acl` omit source ownership and grants.

Then compare table counts, representative records, constraints, and sequence values. For a table named `items`, compare these results:

```bash
psql "$SOURCE_DATABASE_URL" -tAc 'SELECT count(*) FROM items;'
psql "$DESTINATION_DATABASE_URL" -tAc 'SELECT count(*) FROM items;'
```

The counts should match for unchanged test data. During a rehearsal, writes to the live source after the dump can produce different counts. At the final cutover, pause writers before exporting and compare the restored data while they remain paused. Writes made to Railway after the rehearsal still need to be transferred at cutover.

### 6\. Configure the Python database client

For SQLAlchemy with psycopg 3, adapt the API's existing database module:

```python
import os

from sqlalchemy import create_engine
from sqlalchemy.engine import make_url

database_url = make_url(os.environ["DATABASE_URL"]).set(
    drivername="postgresql+psycopg"
)
engine = create_engine(
    database_url,
    pool_size=1,
    max_overflow=0,
    pool_pre_ping=True,
)
```

Use `postgresql+psycopg` for psycopg 3, as shown above, or `postgresql+psycopg2` if the application retains psycopg2.

Each application instance creates its own connection pool, so keep one engine per instance and close checked-out connections after each operation. The example starts with a small pool; tune its size against the provider's connection budget and expected concurrency across all instances.

### 7\. Test the API locally with `vercel dev`

If the API uses a database, apply the migrations from step 10 to Development using its direct connection URL, then add the test records your routes need. The Preview restore from step 5 does not populate this separate database.

Pull the project's Development configuration, then run the API:

```bash
vercel pull --environment=development
uv run vercel dev
```

Use the local URL printed by the CLI. At its default port, the health check is:

```bash
curl --fail http://localhost:3000/health
```

For the entrypoint example, the expected body is:

```json
{"status":"ok"}
```

Exercise a database-backed route such as `/api/items/1` against a known test record. Check cache reads and writes as well as API authentication, because the health route alone doesn't test those dependencies.

### 8\. Adapt queue-driven workers to Vercel Queues

Migrate Railway workers running `celery -A tasks worker` to queue-triggered subscribers through the supported Celery integration. Keep the Railway worker on the old queue until the handoff in step 12.

Vercel Queues is in Beta, and the [Celery integration](https://vercel.com/docs/frameworks/backend/celery) requires Vercel CLI 58.9.0 or later. For an existing Celery application, change its initialization in `tasks.py` and retain the task definitions below it:

```python
from celery import Celery

app = Celery(
    "my-api",
    broker="vercel://",
    backend="vercel-runtime-cache://",
)
app.conf.task_acks_late = True
```

Create a `worker.py` file at the project root that exposes the Celery application as the subscriber entrypoint:

```python
from tasks import app

__all__ = ["app"]
```

Merge these tables into `pyproject.toml`, using your actual web entrypoint. Celery projects declare it explicitly alongside the subscriber, even for a root-level `main.py`. If step 2 already added `[tool.vercel]`, retain that table and add `[[tool.vercel.subscribers]]` below it.

```toml
[tool.vercel]
entrypoint = "main:app"

[[tool.vercel.subscribers]]
entrypoint = "worker:app"
```

In `tasks.py`, declare every routed queue through `task_queues` or `task_default_queue` before the build imports the application. Queues named only in `task_routes` won't get a subscriber.

Restart `vercel dev` to load the new configuration and run the web application and subscriber together. Then call the route that enqueues work, such as `POST /api/reports`, and verify the task's completed result or database side effect.

Tasks can be delivered more than once, so use an idempotent write strategy such as a unique report identifier. Late acknowledgments protect against timeout or crash loss; configure explicit retries for application exceptions. Store task results that need to persist in Postgres or a supported durable backend because the Runtime Cache result backend is ephemeral.

Replace `celery beat` with a cron route that enqueues work. Celery holds `countdown` and `eta` tasks in an invocation until their scheduled time; the delay and execution must fit its duration limit.

For Dramatiq, follow the [Dramatiq configuration](https://vercel.com/docs/frameworks/backend/dramatiq). Other queue libraries need their own adaptation or a worker retained on Railway.

### 9\. Replace Railway cron services with cron routes

Railway cron services that run `python -m jobs.cleanup` need an HTTP route on Vercel. Preserve the cleanup function and change how it is invoked.

Refactor `jobs/cleanup.py` so it exports a `run()` function containing the existing job logic. Retain a `main` guard if Railway still runs the module. This example assumes `run()` returns the number of deleted records. Add the following imports and route to your existing FastAPI `main.py`:

```python
import hmac
import os

from fastapi import Header, HTTPException
from jobs.cleanup import run as cleanup

@app.get("/api/cron/cleanup")
def cleanup_job(authorization: str | None = Header(default=None)):
    secret = os.environ.get("CRON_SECRET")
    if not secret:
        raise HTTPException(status_code=503, detail="Cron secret is not configured")
    expected = f"Bearer {secret}"
    if not authorization or not hmac.compare_digest(
        authorization.encode(), expected.encode()
    ):
        raise HTTPException(status_code=401, detail="Unauthorized")
    if os.environ.get("JOBS_ENABLED") != "true":
        return {"status": "disabled"}
    return {"status": "ok", "deleted": cleanup()}
```

Set a random `CRON_SECRET` in Vercel's environment variables and leave `JOBS_ENABLED=false` in Production until the scheduler handoff. This example's `JOBS_ENABLED` variable prevents the route from doing work before cutover.

Merge the schedule into `vercel.json`:

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "crons": [
    {"path": "/api/cron/cleanup", "schedule": "0 3 * * *"}
  ]
}
```

[Vercel Cron Jobs](https://vercel.com/docs/cron-jobs/manage-cron-jobs) send an HTTP GET to the production route. When `CRON_SECRET` is set, the request includes it as a bearer token in the `Authorization` header, which the route above checks. Schedules use UTC and numeric values; translate named days or months. The example schedules a daily run at 03:00 UTC.

On Hobby, invocation occurs within that hour, and more frequent schedules fail deployment. Pro and Enterprise support per-minute schedules, with a limit of [100 cron jobs per project](https://vercel.com/docs/cron-jobs/usage-and-pricing) on every plan.

Make the cleanup operation safe to repeat, and use a lock if overlapping runs would cause problems. Cron delivery is best effort, and failed invocations aren't retried automatically. Monitor failures and provide a way to rerun missed work.

Test missing and incorrect credentials for a 401 response. In an isolated test environment with `JOBS_ENABLED=true`, a valid request should return `status: ok` and the deletion count.

### 10\. Move pre-deploy and startup migrations to CI

Move commands from Railway's [`deploy.preDeployCommand`](https://docs.railway.com/config-as-code/reference) into the release pipeline, and check the generated start command for migration work too. Railpack's Django startup can run `python manage.py migrate`, so an application can depend on startup migrations without an explicit pre-deploy setting.

Run migrations once in CI after restoring the destination database and before promotion. Rehearse against Preview; apply them to Production after the final restore in step 12. The job must check out the deployment's commit, install the pinned Python version and uv, and run from the application root.

Bind the CI secret `DATABASE_URL_UNPOOLED` to the destination's direct URL. Set it explicitly in the shell for a local rehearsal; `vercel pull` doesn't export shell variables. If Alembic reads `DATABASE_URL`, use this command:

```bash
: "${DATABASE_URL_UNPOOLED:?Set the intended direct database URL}" &&
uv sync --locked &&
DATABASE_URL="$DATABASE_URL_UNPOOLED" uv run --locked alembic upgrade head
```

Make deployment promotion depend on the migration job succeeding. If Alembic reads a URL from another setting, update `alembic/env.py` to read the intended variable; setting a shell variable alone won't change that configuration. Apply the explicit SQLAlchemy driver selection in the migration configuration too. For Django, run `python manage.py migrate` with database settings for the same destination.

Keep schema changes compatible with the old application while it is still running. Avoid migrations in request handlers, application startup, or a preview build that can reach production data.

### 11\. Verify a preview deployment

Check the linked project's deployment history first. [The first deployment of a new project targets Production](https://vercel.com/docs/cli/deploy#prod), even without `--prod`.

If no successful Production deployment exists, temporarily connect the Production environment only to isolated test services, set `JOBS_ENABLED=false`, and disable startup work and task producers. Keep the final production database empty. Create the initial deployment without assigning production domains:

```bash
vercel deploy --prod --skip-domain
```

Once initialization succeeds, deploy the migration branch to Preview. Projects with an existing successful Production deployment can start here:

```bash
PREVIEW_URL="$(vercel deploy)"
```

`vercel deploy` prints the URL to standard output, so the command stores it in `PREVIEW_URL`. Confirm in the dashboard that this deployment targets Preview and uses the intended test services before testing its routes. Step 12 restores Production configuration to the final backing services. Inspect the Preview deployment and test the health route:

```bash
vercel inspect "$PREVIEW_URL" --logs
vercel curl /health --deployment "$PREVIEW_URL"
vercel logs --deployment "$PREVIEW_URL" --query 'level:error'
```

The build log should show your pinned Python minor version and successful dependency installation. The health route should return 200. `vercel curl` supports testing a deployment protected by Vercel's Deployment Protection.

Verify authenticated routes and representative database writes on preview data. Enqueue a task and wait for its result. Call the cron route manually with test credentials; scheduled invocation only happens in Production. Confirm that long requests and representative uploads fit the configured limits.

If a route needs longer than the 300s default, merge a function entry keyed by the resolved entrypoint file into `vercel.json`. This example uses the 800s maximum available on Pro and Enterprise:

```json
{
  "functions": {
    "main.py": {"maxDuration": 800}
  }
}
```

Use your actual filename and retain the `crons` entry. Test any FastAPI lifespan cleanup that the API relies on; [shutdown has a 500 ms window](https://vercel.com/docs/frameworks/backend/fastapi), so durable work shouldn't depend on a long shutdown handler.

### 12\. Cut over production traffic

Set Production variables to the final backing services and keep `JOBS_ENABLED=false`. The staged deployment uses these values before domain assignment. Protect access to its deployment URL and keep write routes and task-producing requests disabled until the final restore and read-only checks succeed.

Audit startup work and queue producers; `JOBS_ENABLED` gates only the example cron route. Stage without assigning production domains:

```bash
PRODUCTION_DEPLOYMENT_URL="$(vercel deploy --prod --skip-domain)"
```

The command stores the staged deployment's URL in `PRODUCTION_DEPLOYMENT_URL` for promotion after the final data transfer.

Schedule a write pause for the final dump and restore:

1. Stop writes through the Railway API, including uploads, and disable its schedules and task producers.
   
2. Wait for in-flight API requests and running tasks to finish, and account for pending, delayed, or retrying messages. Stop the old queue consumers and confirm that all writers are paused before exporting. Replay retained work only against the chosen destination.
   
3. Confirm that `SOURCE_DATABASE_URL` still identifies Railway and set `DESTINATION_DATABASE_URL` to the final Production database's `DATABASE_URL_UNPOOLED`. Check the destination's project, branch, and database name before restoring. If it contains rehearsal data, back up anything you need and recreate only that disposable destination; never clear a database that has accepted production writes.
   
4. Take a fresh export, restore it into the empty destination, and run the required migrations. If recreating the destination changed its endpoint or credentials, update the Production variables and repeat the staging command above to replace `PRODUCTION_DEPLOYMENT_URL`. Keep writers paused if any step fails.
   
5. Synchronize files added, changed, or deleted since the rehearsal. Skip this transfer if both deployments already use the same destination storage.
   

Verify the final database and files before enabling writes on Vercel. If Redis holds sessions or durable records, complete the rehearsed transfer while its writers remain paused, preserve required key expirations, and validate the destination. Apply the planned session invalidation instead if that is the chosen handoff. Enable the new writers only after these checks pass.

Promote the staged deployment after testing its routes against the final data:

```bash
vercel promote "$PRODUCTION_DEPLOYMENT_URL"
```

[Add the custom domain](https://vercel.com/docs/domains/working-with-domains/add-a-domain) to your Vercel project and apply the DNS records shown. Confirm HTTPS and API behavior through the domain. Update allowed origins, callback URLs, and webhook destinations that use the Railway hostname.

Keep the old application from writing to its old database while cached DNS records still route traffic there.

When the Railway scheduler is disabled and its work has finished, set `JOBS_ENABLED=true` for Production and redeploy. Confirm one scheduled execution and check for duplicate work. Environment-variable changes require a new deployment to take effect.

### 13\. Retire unused Railway resources after the rollback window

Keep Railway resources available for the rollback window you chose before cutover. Monitor errors, database connections, and task completion under production traffic before removing unused services.

> **Warning:**
> 
> If Vercel has accepted new writes, sending traffic back to the old application and database can lose those changes. Reconcile data or point the compatible old application at the authoritative database before reverting traffic. Vercel's application rollback doesn't reverse database migrations or restore data.

After the rollback window, retain the required backups, remove unused Railway services, and clean up source-specific configuration. Delete local secret exports and database dumps when they are no longer needed.

## What doesn't map from Railway

Railway volumes need an external storage destination. Workers that poll continuously or depend on a persistent process need to stay on Railway or be redesigned as bounded work.

Railway restart-policy settings don't translate into Python application configuration on Vercel. Recreate the checks performed by `healthcheckPath` against a deployment URL, and run pre-deploy migrations in your pipeline. The native entrypoint imports the app without executing the old start command.

## Troubleshooting

### The build uses an unexpected Python version

Check `.python-version` at the configured project root and any `requires-python` constraint. Any remaining `RAILPACK_PYTHON_VERSION` value doesn't replace those files. Inspect the build log after redeploying.

### A dependency needs a system library

Compare the failing package with Railpack's system dependency list. Pin a compatible wheel or use the container path when the application needs an operating-system package. Don't assume a package installed by Railpack is present on the native runtime.

### Every route returns 404

Confirm the Vercel project's root directory contains the dependency manifest and entrypoint. Check the exported application name and custom `tool.vercel.entrypoint`, then inspect the deployment's detected framework. Avoid carrying over container rewrites that point away from the native app.

### Requests exceed the payload or duration limit

For a `413 FUNCTION_PAYLOAD_TOO_LARGE`, upload directly to object storage. For a `504 FUNCTION_INVOCATION_TIMEOUT`, inspect slow database or external-service calls before increasing `maxDuration` within the plan's limit. Split work that still exceeds the limit into shorter tasks.

### Database connections fail or the connection pool fills up

Use destination connection values in place of Railway private hostnames or reference expressions. For requests, use the provider's pooled URL and keep total pool capacity across instances within its connection limit.

### A cron route works manually but doesn't run on a schedule

Check that the schedule is deployed to Production, its timezone is UTC, and `JOBS_ENABLED` is true in the active deployment. Preview URLs let you test the handler but don't receive scheduled invocations.

## Next steps

- [Python runtime reference](https://vercel.com/docs/functions/runtimes/python)
  
- [Deploy a FastAPI app on Vercel](https://vercel.com/docs/frameworks/backend/fastapi)
  
- [Run Celery on Vercel](https://vercel.com/docs/frameworks/backend/celery)
  
- [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs)
  
- [How to migrate from Railway to Vercel](https://vercel.com/kb/guide/migrate-railway-to-vercel)
  
- [Deploy Python apps on Vercel using Docker](https://vercel.com/kb/guide/vercel-docker-python-apps)
  

## Frequently asked questions

### Do I need a Dockerfile for a Python API built with Railpack?

No. FastAPI, Flask, and Django applications with compatible dependencies can use Vercel's native Python runtime. Choose a container when the application needs system packages or another dependency the native build cannot supply.

### What replaces railway.json on Vercel?

Translate each setting according to its job. Python dependencies and entrypoints belong in the supported manifest or application files, while cron configuration and function duration belong in `vercel.json`. Keep the Railway file until the source deployment no longer needs it.

### Where should Django migrations run after leaving Railway?

Run migrations in a controlled pipeline step before releasing the application. Check Railpack's start command as well as `preDeployCommand`, because Railpack can include Django migrations in startup. Vercel's native entrypoint doesn't execute that start command.

### Can my Celery worker service move to Vercel?

Celery services can use configured subscribers on Vercel Queues, which is in Beta. Check task duration, retry behavior, and result persistence before replacing the service. Account for pending work in the Railway broker during the handoff in step 12 before retiring the old service.

### How do I export Railway Postgres for the migration?

Enable the database's public TCP Proxy and use its external connection URL with `pg_dump`. Restore into an empty destination and validate the data. Repeat the transfer under a controlled write pause at cutover so later writes aren't lost.

## More Vercel Functions guides

- [Migrate a Python API from Render to Vercel](/kb/guide/migrate-a-python-api-from-render-to-vercel): Move a FastAPI, Flask, or Django API from Render to Vercel Functions. Migrate configuration, data, workers, and scheduled jobs.
- [Migrate a Python API from Heroku to Vercel](/kb/guide/migrate-a-python-api-from-heroku-to-vercel): Move a FastAPI, Flask, or Django API from Heroku to Vercel Functions. Migrate configuration, data, workers, and scheduled jobs.
- [Build Figma-style multiplayer cursors with WebSockets on Vercel](/kb/guide/real-time-board-nextjs-fastapi): Learn how to build Figma-style multiplayer cursors with Next.js and FastAPI, kept consistent across multiple Vercel Function instances using WebSockets and Redis.