Skip to content

Setup ​

In one sentence

Pick SQLite for the fastest start with no Docker, or Docker and Postgres to reproduce the captured headline numbers; both run the same code.

You will run two processes: the payment gateway stand-in, and the Orders API. The Orders API calls the gateway in AP1 and AP2.

Choose a mode ​

SQLite (no Docker)Docker and Postgres
Databasedb/orders.db, a local filePostgres 16 in a container
Gatewaymock_gateway/ (bundled)go-httpbin in a container
Use it forTrying every demo quicklyThe numbers in the talk and benchmarks
Relative numbersAP2 gap is smaller (about 4×)AP2 gap is about 10×

The SQLite gap is smaller because a new SQLite engine has no network handshake. See Benchmarks for the full explanation.

Mode 1: SQLite, no Docker ​

Run these from orders-api-demo/. You need uv.

bash
cd orders-api-demo
uv sync
uv run python scripts/seed_data.py     # creates db/orders.db

Seeding is idempotent. It prints "Data already seeded, skipping" on a second run. Delete db/orders.db to reseed. The seed creates 200 customers, 2000 orders, and about 6000 order items.

Start the two services in two terminals:

bash
uv run uvicorn mock_gateway.main:app --port 8080
bash
uv run uvicorn app.main:app --port 8000

Check both services:

bash
curl localhost:8000/health    # {"status":"ok"}
curl localhost:8080/health    # {"status":"ok"}

Expected output:

json
{"status":"ok"}

Mode 2: Docker and Postgres ​

This mode matches the captured Postgres numbers. Docker and uv are required.

bash
cd orders-api-demo
cp .env.example .env
docker compose up -d          # postgres:16, go-httpbin gateway, and the app
uv sync                       # local venv for running scripts and tests on the host

DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/orders" \
  uv run python scripts/seed_data.py

curl http://localhost:8000/health
curl "http://localhost:8000/orders?limit=5"

The compose file starts three services:

ServiceImageHost port
dbpostgres:165432
payment-gatewaymccutchen/go-httpbin:v2.15.08080
appbuilt from orders-api-demo/Dockerfile8000

Interactive API docs are at http://localhost:8000/docs once the app is running.

Docker-only commands

scripts/profile_pyspy.sh runs docker compose exec, so it only works in this mode. The flame graphs in benchmarks/ were produced this way.

Configuration ​

Settings are read in app/config.py through pydantic-settings. Environment variables override .env.

VariableDefault in code.env.exampleMeaning
DATABASE_URLsqlite+aiosqlite:///<repo>/orders-api-demo/db/orders.dbpostgresql+asyncpg://postgres:postgres@db:5432/ordersSQLAlchemy URL
PAYMENT_GATEWAY_URLhttp://localhost:8080http://payment-gateway:8080Base URL for the gateway
POOL_MODEgoodgoodPool profile, bad or good, read at process start

The pool profiles are fixed in code:

POOL_MODEpool_sizemax_overflowpool_timeoutpool_recycle
bad5102 s-1
good204010 s1800

Gotchas ​

These are the problems people hit first. Read them before you start.

PAYMENT_GATEWAY_URL points at the Docker hostname

If your .env contains PAYMENT_GATEWAY_URL=http://payment-gateway:8080, every /demo/ap1/* and /demo/ap2/* call returns HTTP 500 with nodename nor servname provided when you run locally. The hostname only resolves inside Docker.

Fix: for local runs, set PAYMENT_GATEWAY_URL=http://localhost:8080 in .env, or prefix the command. Environment variables beat .env:

bash
PAYMENT_GATEWAY_URL=http://localhost:8080 uv run uvicorn app.main:app --port 8000

POOL_MODE is read once, at startup

/demo/ap5/info reports the pool that the process actually started with. If it says bad when you wanted good, the .env value won, or an earlier process is still running.

Fix: set the variable on the command and confirm:

bash
POOL_MODE=good uv run uvicorn app.main:app --port 8000
curl localhost:8000/demo/ap5/info

DATABASE_URL pointing at db:5432 breaks SQLite

A .env with the Docker DATABASE_URL will not work outside Docker. Comment it out for SQLite; the default is db/orders.db.

Port already in use

Address already in use, or strange 404s on port 8000, means another process owns the port.

bash
lsof -nP -iTCP:8000 -sTCP:LISTEN

Stop that process, or run uvicorn on another port and adjust the URLs.

.env is gitignored, so edit it freely for your machine.

Verify the setup ​

Run the automated tests. They start the mock gateway themselves if nothing is listening on PAYMENT_GATEWAY_URL.

bash
PAYMENT_GATEWAY_URL=http://localhost:8080 uv run pytest tests/ -v

Expected: 13 passed (about 5 seconds). If your .env still has the Docker hostname, the export above overrides it.

Then run the first demo:

bash
time (for i in 1 2 3; do curl -s -o /dev/null localhost:8000/demo/ap1/bad & done; wait)

Expected: about 3.0 s in the SQLite mode with the local gateway. The AP1 page shows the captured Postgres run.

Released under the MIT License. Speaker: Satyam Soni, PyCon Hong Kong 2026.