# How to Deploy FastAPI with a Managed PostgreSQL Database
FastAPI has become the default for modern Python APIs thanks to its async core, automatic OpenAPI docs, and Pydantic validation. But its speed advantage evaporates if the database layer blocks the event loop or mismanages connections. This tutorial covers deploying FastAPI with a managed PostgreSQL the right way — async drivers, proper pooling, Alembic migrations, and a production server.
Step 1: async database setup
To keep FastAPI non-blocking, use an async driver (asyncpg) with async SQLAlchemy. Read the connection string from the environment:
# db.py
import os
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
# Note the +asyncpg driver in the URL scheme
DATABASE_URL = os.environ["DATABASE_URL"].replace(
"postgresql://", "postgresql+asyncpg://", 1
)
engine = create_async_engine(
DATABASE_URL,
pool_size=10, # tune to your DB connection limit
max_overflow=5,
pool_pre_ping=True, # drop dead connections gracefully
)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)pool_pre_ping=True is important in the cloud — it transparently recycles connections that the database closed (common with managed Postgres idle timeouts).
Step 2: a session dependency
FastAPI's dependency injection gives each request a clean session:
from fastapi import Depends
async def get_db():
async with SessionLocal() as session:
yield session
@app.get("/todos")
async def list_todos(db = Depends(get_db)):
result = await db.execute(select(Todo))
return result.scalars().all()Step 3: mind the connection limit
This is the most common FastAPI-with-Postgres production mistake. Total connections = pool_size × number of app instances. If your managed DB allows 50 connections and you run 5 replicas with pool_size=10, you are at the ceiling with no headroom. Size deliberately:
max connections used = (pool_size + max_overflow) × replicasKeep it comfortably below your plan's connection limit, or front the database with a pooler like PgBouncer for high replica counts.
Step 4: migrations with Alembic
Never create tables by hand in production. Use Alembic, configured to read the same env var:
# alembic/env.py (key line)
config.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])# Generate a migration after model changes
alembic revision --autogenerate -m "add todos table"
# Apply migrations as a release step
alembic upgrade headRun alembic upgrade head *before* new code serves traffic, and keep migrations backward-compatible for rolling deploys.
Step 5: the production server
FastAPI runs on an ASGI server. Use Uvicorn, optionally managed by Gunicorn workers, and read the port from the environment:
# Simple, single-process-friendly
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}
# Multi-worker via Gunicorn + Uvicorn workers
gunicorn app.main:app -k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:${PORT:-8080} --workers 3Remember: each worker has its own pool, so count them in the connection math above.
Step 6: containerize
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8080}Step 7: health check
Give the platform something to probe — and make it verify the DB:
@app.get("/health")
async def health(db = Depends(get_db)):
await db.execute(text("SELECT 1"))
return {"ok": True}Common pitfalls
- Sync driver in async code. Using
psycopg2synchronously blocks the event loop and kills FastAPI's concurrency. Useasyncpgfor async paths. - Connection exhaustion. Too many replicas × pool size > DB limit. Do the math.
- Tables created at startup instead of migrations. Causes drift and race conditions across replicas.
- No
pool_pre_ping. Stale connections throw intermittent errors after idle periods. - Hardcoded port. Always read
PORT.
Deploying on PandaStack
This is squarely PandaStack's golden path: "Push code. It runs." Connect your repo and PandaStack auto-detects the Python app (or builds your Dockerfile). Provision a managed PostgreSQL (14.x or 16.x) and PandaStack auto-wires it — DATABASE_URL is injected, which both SQLAlchemy and Alembic read directly, so there is no manual connection wiring. Mind the plan's connection limits when sizing your pool: the free tier allows 50 DB connections, Pro 300, Premium 1000 — match pool_size × replicas to your tier. You get scheduled and manual backups, custom domains with automatic SSL for your API, live build/app logs to watch alembic upgrade, plus rollbacks. The free tier (5 web services + 1 database, 50 connections) is enough for a complete FastAPI + Postgres service.
References
- [FastAPI SQL databases (async)](https://fastapi.tiangolo.com/tutorial/sql-databases/)
- [SQLAlchemy async ORM documentation](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
- [asyncpg](https://magicstack.github.io/asyncpg/current/)
- [Alembic documentation](https://alembic.sqlalchemy.org/)
- [Uvicorn deployment](https://www.uvicorn.org/deployment/)
---
FastAPI plus a managed Postgres is fast and clean once pooling and migrations are right. PandaStack auto-wires the database and builds your API from one git push — deploy free at [dashboard.pandastack.io](https://dashboard.pandastack.io).