| name | deploying-fastapi-to-vercel |
| description | Use when deploying a FastAPI or Python ASGI/WSGI app to Vercel, choosing uv vs requirements.txt, pinning Python version, or when a Vercel URL returns "Redirecting..." / 302 to SSO. Covers the auto-detect Python runtime and serverless limits. |
Deploying FastAPI to Vercel
Verified live 2026-06-25 (Vercel CLI 54.17.1, Python 3.12, FastAPI 0.117.1, uv).
Core principle
Vercel's Python runtime loads a top-level FastAPI instance named app and routes
every path to it as ONE Vercel Function (Fluid compute, autoscaling). No
vercel.json, no uvicorn in production, no adapter. Vercel uses uv under the
hood to build.
Your API runs as a serverless function, not a long-lived server: no always-on
process, every request has a hard timeout, and you cannot do background work after
the response is sent. Design for the Limits below.
Ignore older guides that use "builds" + "routes" + "@vercel/python" in
vercel.json. That is legacy config and is no longer needed.
Two non-negotiable rules
- The instance MUST be named
app (Django/WSGI use application).
- The file MUST be at a detected entrypoint:
app.py | index.py | server.py | main.py | wsgi.py | asgi.py, optionally inside src/, app/, or api/.
For any other path, point Vercel at it in pyproject.toml under
[tool.vercel] with entrypoint = "module:app" (e.g. routes:app for a
root-level routes.py, backend.server:app for backend/server.py).
Minimum files (uv path, preferred)
main.py # app = FastAPI()
pyproject.toml # [project] dependencies = ["fastapi==..."], requires-python
.python-version # e.g. 3.12 (default 3.12; 3.13, 3.14 also available)
Run uv lock to produce uv.lock; Vercel installs from it. A copy-ready set is
in example/ next to this file.
requirements.txt is the simple alternative (one line: fastapi==0.117.1). Use
ONE dependency source, not both. You do NOT need requirements.txt if you use
pyproject/uv.
Commands
vercel deploy --yes
vercel --prod --yes
vercel dev
Always pass --yes in scripts/agents so the CLI never blocks on prompts. Min CLI
version 48.1.8. Build-log signals to confirm: Detected FastAPI, Using Python 3.12 from .python-version, Installing required dependencies from uv.lock.
GOTCHA: "Redirecting..." / 302 to vercel.com/sso-api is NOT a bug
New projects have Deployment Protection on by default. The unique
deployment URL (<proj>-<hash>.vercel.app) redirects to Vercel SSO and looks
totally broken when curled. The production alias (<proj>.vercel.app) is
public and serves real responses. So: smoke-test the production alias, not the
generated URL. To open the generated/preview URLs for programmatic access,
disable Deployment Protection (Project → Settings) or send a Protection Bypass
token in the x-vercel-protection-bypass header.
Verify (don't trust a 200 on /)
Prove the WHOLE app is mounted, not one handler. Use example/smoke_test.sh <url>:
it checks root, a nested path param, /openapi.json (every route registered), and
a POST with a Pydantic body (request parsing in the serverless env). Re-run after
every deploy as a regression gate.
Limits that bite real apps
| Limit | Consequence |
|---|
| Max duration (hard timeout) | Default 300s (5 min). Hobby capped at 300s; Pro/Ent up to 800s (1800s in beta). Exceeding it returns 504 FUNCTION_INVOCATION_TIMEOUT. No always-on server, no background work after the response returns. Unlimited runtime needs Vercel Workflows. Raise it via vercel.json maxDuration (see below). |
| Request/response body ≤ 4.5 MB | Larger payloads return 413 FUNCTION_PAYLOAD_TOO_LARGE. Stream, or move big files direct to object storage (Vercel Blob/S3) instead of through the function. |
| 500 MB uncompressed bundle | A torch/TF/large-ML stack won't fit. __pycache__/.pyc stripped; nothing else tree-shaken. Trim with excludeFiles in vercel.json. |
| Stateless / no shared memory | Fresh instance per request possible. No in-process cache, global counters, or threads outliving the request. Use Redis/DB/KV. |
Read-only FS except /tmp | Only /tmp is writable, and not durable across invocations. |
| Lifespan shutdown ≤ 500 ms | lifespan shutdown cut off 500 ms after SIGTERM; its logs don't show in the dashboard. |
| DB connections | Serverless opens many short-lived connections; use a pooler (PgBouncer/Neon/Supabase pooled URL) or an HTTP data API. |
| Cold starts | Keep top-level imports lean; do heavy init lazily. |
When you DO need vercel.json
Only for what auto-detect can't infer. The two common cases for a FastAPI app:
setting the function timeout and trimming the bundle. Python CANNOT set
maxDuration in code (Node/Next can), so it goes here. The glob targets your
entrypoint (e.g. main.py):
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"functions": {
"**/*.py": {
"maxDuration": 60,
"excludeFiles": "{tests/**,**/test_*.py,fixtures/**,data/**}"
}
}
}
(maxDuration is in seconds and must be within your plan's max.)
Other notes
- Static assets: put in
public/** (CDN-served). Do NOT app.mount("/public", ...).
- Env vars:
vercel env add NAME production; vercel env pull .env.local for local.
- Inspect failures:
vercel inspect <url> --logs (failed pip/uv install or an
import error at module load is the usual culprit).
FastAPI backend + a frontend in ONE deployment
The single-function recipe is for FastAPI ALONE. Bundling a Python API with a
separate frontend (Vite/Next) in one project is Vercel Services (multiple
build outputs), a different setup. Otherwise deploy the API as its own project and
point the frontend at its URL.
References