Back to Blog
Tutorial10 min read2026-06-30

How to Deploy FastAPI with a Managed PostgreSQL Database

FastAPI's async speed shines only when the database layer keeps up. This tutorial covers async SQLAlchemy, connection pooling, migrations with Alembic, and deploying FastAPI with a managed PostgreSQL.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# 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) × replicas

Keep 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 head

Run 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 3

Remember: 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 psycopg2 synchronously blocks the event loop and kills FastAPI's concurrency. Use asyncpg for 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 itDATABASE_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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also