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

Bash
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:

Bash
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

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"]
Text
# requirements.txt
fastapi
uvicorn[standard]
gunicorn
Bash
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 --reload on 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.