Cheatsheet for connecting SQLAlchemy to Postgres. Long-form: Postgres textbook Ch 1 .
Driver matrix
| Driver | Sync/Async | Use |
|---|---|---|
| asyncpg | async | Default for async; fastest |
| psycopg (v3) | both | Native async; closer to libpq |
| psycopg2 | sync | Legacy |
| pg8000 | sync | Pure Python; libpq-free |
URL
"postgresql+asyncpg://user:pass@host:5432/db"
"postgresql+psycopg://user:pass@host:5432/db" # v3
"postgresql+psycopg2://user:pass@host:5432/db"
connect_args (asyncpg)
engine = create_async_engine(URL, connect_args={
"server_settings": {
"application_name": "myapp",
"search_path": "myapp,public",
"timezone": "UTC",
"statement_timeout": "30s",
"idle_in_transaction_session_timeout": "60s",
},
"command_timeout": 60,
"statement_cache_size": 1000,
})
connect_args (psycopg v3)
engine = create_async_engine(URL, connect_args={
"options": "-c search_path=myapp,public -c application_name=myapp -c timezone=UTC",
})
PgBouncer (transaction-pool mode) — required tweaks
engine = create_async_engine(
"postgresql+asyncpg://app@pgbouncer:6432/db",
pool_size=10,
max_overflow=10,
pool_pre_ping=True,
pool_recycle=300,
connect_args={
"statement_cache_size": 0, # required
"prepared_statement_cache_size": 0,
"server_settings": {"jit": "off"},
},
)
Without these: random “prepared statement does not exist” errors.
TLS
engine = create_async_engine(URL, connect_args={
"ssl": "require", # asyncpg: "disable" | "allow" | "prefer" | "require" | "verify-ca" | "verify-full"
})
For verify-full:
import ssl
ctx = ssl.create_default_context(cafile="/path/to/ca.crt")
engine = create_async_engine(URL, connect_args={"ssl": ctx})
RDS IAM auth
@event.listens_for(engine.sync_engine, "do_connect")
def fetch_token(dialect, conn_rec, cargs, cparams):
cparams["password"] = generate_iam_token(host, port, user, region)
Token refreshes per connection.
Cloud SQL Auth Proxy
Run proxy as a sidecar; connect to localhost.
application_name (debugging)
SELECT pid, application_name, state, query
FROM pg_stat_activity
WHERE application_name = 'myapp';
Each app’s connections visible. Set distinct names per service.
search_path per session
@event.listens_for(engine.sync_engine, "connect")
def on_connect(dbapi_conn, conn_record):
cur = dbapi_conn.cursor()
cur.execute("SET search_path TO myapp_schema, public")
cur.close()
For schema-per-tenant: rotate per request via SET LOCAL.
statement_timeout
# Per-connection (durable)
@event.listens_for(engine.sync_engine, "connect")
def on_connect(dbapi_conn, conn_record):
cur = dbapi_conn.cursor()
cur.execute("SET statement_timeout = '30s'")
cur.close()
# Per-transaction
async with session.begin():
await session.execute(text("SET LOCAL statement_timeout = '60s'"))
await session.execute(slow_stmt)
idle_in_transaction_session_timeout
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
Kills connections holding open transactions while idle. Prevents lock pile-ups.
Read replicas
write_engine = create_async_engine(WRITER_URL, pool_size=20)
read_engine = create_async_engine(
READER_URL,
pool_size=40,
connect_args={"server_settings": {"default_transaction_read_only": "on"}},
)
Multi-host failover
"postgresql+asyncpg://app@primary:5432,replica:5432/db?target_session_attrs=read-write"
asyncpg picks the writable host; handles failover.
Patroni / Stolon
App connects to a stable endpoint (HAProxy / pgcat). Patroni manages failover behind it.
Verify pool
print(engine.pool.size(), engine.pool.checked_out())
Expose as Prometheus gauge.
Common mistakes
- Mixing psycopg2 with async engine — fails.
- PgBouncer transaction-pool without disabling prepared-stmt cache.
- TLS without server cert verification — MITM risk.
- Generic
application_name(or unset) — debugging nightmares. - Forgetting
statement_timeout— runaway queries.
Read this next
If you want my Postgres engine + RDS IAM template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .