Back to Blog
Tutorial10 min read2026-07-03

How to Deploy a Sanic Python API to Production

Sanic is a fast async Python web framework with its own production server. Learn how to deploy a Sanic API with multiple workers, a database connection pool, and a tuned container.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Sanic was one of the first Python frameworks built around async/await and it ships its own production-grade server — no Gunicorn or Uvicorn required. That self-contained server model changes how you deploy it. This guide covers a clean production deployment of a Sanic API with a database pool.

Sanic's built-in server

Unlike FastAPI (which needs an external ASGI server), Sanic includes a high-performance server that handles multiple worker processes natively. You launch it with the sanic CLI or app.run(). In production, the CLI with --fast or an explicit worker count is the standard approach.

A minimal Sanic app

# server.py
from sanic import Sanic, json

app = Sanic("MyAPI")

@app.get("/")
async def index(request):
    return json({"message": "Hello from Sanic"})

@app.get("/health")
async def health(request):
    return json({"status": "ok"})

Running in production

The Sanic CLI is the recommended way to run in production:

sanic server:app --host 0.0.0.0 --port 8000 --workers 4

or let Sanic auto-pick worker count with --fast (one worker per available core):

sanic server:app --host 0.0.0.0 --port 8000 --fast

On a fractional-CPU tier, --fast may spin up only one worker — which is correct. Don't force four workers onto a quarter-core container; you'll just thrash.

Database connection pool with listeners

Sanic's lifecycle listeners are the right place to create and tear down a connection pool. Create the pool once per worker on startup:

import os
import asyncpg
from sanic import Sanic, json

app = Sanic("MyAPI")

@app.before_server_start
async def setup_db(app, _):
    app.ctx.pool = await asyncpg.create_pool(
        dsn=os.environ["DATABASE_URL"],
        min_size=2,
        max_size=10,
    )

@app.after_server_stop
async def close_db(app, _):
    await app.ctx.pool.close()

@app.get("/users/<user_id:int>")
async def get_user(request, user_id):
    async with request.app.ctx.pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
    return json(dict(row) if row else {}, status=200 if row else 404)

With a managed PostgreSQL database linked, DATABASE_URL is injected, and asyncpg accepts the postgres:// DSN directly — no scheme rewriting needed.

Note that each worker creates its own pool, so total connections = workers × max_size. Keep this under your database's connection limit. On a small tier with a 50-connection cap, 4 workers × 10 = 40 connections is fine; bump max_size carefully.

The Dockerfile

FROM python:3.12-slim

ENV PYTHONUNBUFFERED=1
WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD sanic server:app --host 0.0.0.0 --port ${PORT:-8000} --workers ${WEB_CONCURRENCY:-2}

Using the shell form of CMD lets ${PORT} and ${WEB_CONCURRENCY} expand from injected env vars. The --host 0.0.0.0 is mandatory in containers.

Worker count and connection math

Tier CPUSuggested workersasyncpg max_sizeTotal connections
0.25 core155
1 core2816
2 cores4832

Always check the product against your database's connection limit. Scale-to-zero free tiers add a wrinkle: on cold start a fresh worker re-creates its pool, so the first request after idle pays the pool-warmup cost.

Health checks

The /health endpoint should be fast and not touch the database (or do a lightweight SELECT 1 if you want to verify DB connectivity). Point the platform's readiness probe at it.

Graceful shutdown

Sanic's after_server_stop listener (used above to close the pool) ensures rolling deploys don't drop connections mid-flight. Sanic handles SIGTERM and drains in-flight requests before exiting.

Environment variables

VariablePurpose
DATABASE_URLinjected by managed DB link
PORTlisten port
WEB_CONCURRENCYworker count
SANIC_ACCESS_LOGset to false to reduce log noise under load

Deploying

Push your repo, connect it, link a managed PostgreSQL database, set env vars, and deploy. The platform builds the Dockerfile (or auto-detects Python) and ships it. Live build and app logs let you confirm each worker booted and created its pool.

git push origin main

Conclusion

Sanic's self-contained server makes deployment simple: run the CLI binding to 0.0.0.0 and the injected port, create one asyncpg pool per worker via lifecycle listeners, and keep total connections under your database's limit. Its raw async throughput makes it a strong pick for high-concurrency APIs.

Try Sanic with a managed PostgreSQL on PandaStack's free tier — connect your repo at [dashboard.pandastack.io](https://dashboard.pandastack.io) and the database wires itself in.

References

  • [Sanic Documentation](https://sanic.dev/en/)
  • [Sanic: Running Sanic / Workers](https://sanic.dev/en/guide/deployment/running.html)
  • [Sanic: Listeners](https://sanic.dev/en/guide/basics/listeners.html)
  • [asyncpg Connection Pools](https://magicstack.github.io/asyncpg/current/api/index.html#connection-pools)
  • [Python Docker Images](https://hub.docker.com/_/python)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also