FastAPI Production Deployment
Running Uvicorn workers or Gunicorn+Uvicorn workers in production, with a Dockerfile example.
Why the dev server isn't the deployment
uvicorn main:app --reload is perfect for development, but a couple of things change for production: --reload (which watches files and restarts on every change) is pure development overhead in prod, and a single Uvicorn process only uses one CPU core — real traffic needs more than one worker process to use a multi-core machine at all.
Running multiple Uvicorn workers
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
--workers 4 starts four independent worker processes, each running its own copy of the app and its own event loop, with a master process load-balancing incoming connections across them. A reasonable starting point is one worker per CPU core.
Gunicorn as a process manager for Uvicorn workers
A more common production setup uses Gunicorn purely as a battle-tested process manager — restarting a crashed worker automatically, handling graceful reloads — with Uvicorn's own worker class doing the actual ASGI serving:
pip install gunicorn "uvicorn[standard]"
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Plain Uvicorn (--workers N) |
Gunicorn + Uvicorn workers | |
|---|---|---|
| Worker process management | Built into Uvicorn itself | Handled by Gunicorn (mature, widely deployed) |
| Automatic restart of a crashed worker | Limited | Yes — one of Gunicorn's core jobs |
| Zero-downtime reloads | Not built in | Supported (graceful reload via a HUP signal) |
| Extra dependency | None beyond Uvicorn | Both Gunicorn and Uvicorn installed |
Either approach is legitimate; the Gunicorn-managed setup is more common specifically because Gunicorn's process-supervision behavior is older and more battle-tested than reinventing the same thing in Uvicorn's own multi-worker mode.
A Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
# requirements.txt
fastapi
uvicorn[standard]
gunicorn
docker build -t my-fastapi-app .
docker run -p 8000:8000 my-fastapi-app
Copying requirements.txt and installing dependencies before copying the rest of the application code is a deliberate ordering — Docker caches each layer, so as long as requirements.txt doesn't change, a later code-only change reuses the cached dependency-install layer instead of reinstalling everything on every single build.
Common mistakes
- Leaving
--reloadon in a production command — it adds file-watching overhead for no benefit and is documented as a development-only flag. - Running a single worker process in production and wondering why the app doesn't use more than one CPU core under load —
--workers(or a process manager launching several processes) is what actually uses multiple cores; Python's own threading doesn't parallelize CPU-bound work due to the GIL. - Baking secrets (API keys,
SECRET_KEY) directly into the Docker image instead of injecting them at runtime via environment variables — anyone who can pull or inspect the image can extract them.
Interview questions
Q: Why does a production FastAPI deployment typically run multiple worker processes instead of just one?
A single Uvicorn (or Gunicorn) worker process is limited to one CPU core, regardless of how efficiently its event loop handles concurrent I/O within that process — multiple worker processes let the app actually use a multi-core machine, with the parent process distributing incoming connections across them. This is orthogonal to async/await, which helps with concurrency inside one process but doesn't multiply available CPU cores on its own.
Q: What does running Gunicorn with Uvicorn's worker class buy you over running Uvicorn's own --workers flag directly?
Both start multiple worker processes serving the same ASGI app, but Gunicorn contributes its own mature process-management behavior on top — automatically restarting a worker that crashes, and supporting graceful reloads without dropping in-flight connections. Uvicorn's worker class still does the actual ASGI request handling in each process either way; Gunicorn is layered in specifically for supervision, not because Uvicorn can't serve requests on its own.