Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sqla-pool-doctor

Diagnose SQLAlchemy 2.0 async connection-pool problems with one line of code.

sqla-pool-doctor attaches to any AsyncEngine (or sync Engine) and tells you when connections are leaking, when the pool is exhausted, and where in your code the offending checkout happened — with full stack traces.

from sqlalchemy.ext.asyncio import create_async_engine
from sqla_pool_doctor import attach

engine = create_async_engine("postgresql+asyncpg://...")
monitor = attach(engine, leak_threshold_seconds=10)
await monitor.start()  # emit pool stats every 5s and warn on leaks

That's it. The next time a request handler forgets to release a connection, you'll see:

WARNING sqla_pool_doctor: potential connection leak: held 12.3s (threshold 10.0s)
  File "app/handlers.py", line 47, in get_user
    conn = await engine.connect()
  File "app/handlers.py", line 51, in get_user
    return await _fetch(conn, user_id)

Install

Not yet on PyPI — install directly from GitHub:

pip install git+https://github.com/async-workflows/sqla-pool-doctor.git

Pin to a specific commit or tag for reproducibility:

pip install "git+https://github.com/async-workflows/sqla-pool-doctor.git@main"

Try the demo

The package ships with a self-contained demo that intentionally leaks a connection, saturates the pool to capacity, and shows the resulting warnings so you can see the output before integrating it:

pip install "sqla-pool-doctor[demo] @ git+https://github.com/async-workflows/sqla-pool-doctor.git"
sqla-pool-doctor demo

For a live-updating table of the pool snapshot, install the [panel] extra (which pulls in rich) and add --panel:

pip install "sqla-pool-doctor[demo,panel] @ git+https://github.com/async-workflows/sqla-pool-doctor.git"
sqla-pool-doctor demo --panel

If rich is not installed, --panel degrades gracefully to the plain text output with a hint, so the demo always runs.

Usage

Drop-in, fire-and-forget

from sqla_pool_doctor import attach

monitor = attach(engine)
await monitor.start()

Pass a custom logger or thresholds:

import logging

monitor = attach(
    engine,
    leak_threshold_seconds=5.0,
    poll_interval_seconds=2.0,
    logger=logging.getLogger("myapp.db"),
)

One-shot diagnosis

If you don't want a background task, call report() whenever you want a stats line and a leak scan logged on demand:

monitor = attach(engine)
monitor.report()  # logs one pool-stats line + any leak warnings

Health endpoint with snapshot()

snapshot() returns a JSON-serializable PoolSnapshot dataclass — perfect for a /healthz endpoint. It never logs; it just hands you the numbers:

monitor = attach(engine)

@app.get("/healthz/pool")
def pool_status():
    return monitor.snapshot().as_dict()

The snapshot includes the live pool counters, the peak concurrent checkouts observed, running hold-time statistics, and the currently-outstanding checkouts with their age:

{
  "pool_size": 5,
  "checked_out": 2,
  "overflow": 0,
  "checked_in": 3,
  "capacity": 15,
  "outstanding_tracked": 2,
  "peak_checked_out": 6,
  "exhausted": false,
  "hold_time": {"count": 1240, "min": 0.001, "max": 4.12, "mean": 0.031, "p95": 0.12},
  "outstanding": [
    {"age_seconds": 3.4, "stack": "  File \"app/handlers.py\", line 47, ..."}
  ]
}

capacity is size + max_overflow when the pool reports it (e.g. QueuePool), otherwise null. Hold-time stats are in seconds and are updated on every checkin.

Pool-exhaustion warnings

When checked_out reaches the pool's capacity, the monitor logs a single warning per exhaustion episode (it de-dupes and resets once the pool recovers), so a saturated pool won't flood your logs:

WARNING sqla_pool_doctor: pool exhausted: checked_out=15 reached capacity=15 (size + max_overflow); further checkouts will block

Slow-checkout warnings

A checkout held past leak_threshold_seconds is reported as a potential leak. The warning fires immediately at checkin for a connection that was held too long, as well as during the periodic poll loop for one that is still held — with no double-warning for the same checkout.

Alert callback with on_leak

Pass on_leak to receive a callback (in addition to the log warning) whenever a leak is detected — wire it to your metrics, Sentry, or a pager:

def alert(info: dict) -> None:
    metrics.increment("db.pool.leak")
    log.error("connection held %.1fs", info["held_seconds"])

monitor = attach(engine, leak_threshold_seconds=10, on_leak=alert)

The callback receives a dict with held_seconds, threshold_seconds, stack, and conn_id. Callback exceptions are swallowed and logged, so a faulty callback can never break monitoring.

FastAPI lifespan

from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqla_pool_doctor import attach

@asynccontextmanager
async def lifespan(app: FastAPI):
    monitor = attach(app.state.engine, leak_threshold_seconds=10)
    await monitor.start()
    try:
        yield
    finally:
        await monitor.stop()

app = FastAPI(lifespan=lifespan)

What it tracks

Signal When you'll see it
pool stats Every poll_interval_seconds once start() has been called.
connection leak A checkout held longer than leak_threshold_seconds (at checkin too).
pool exhausted checked_out reaches capacity — once per episode, de-duped.
Hold-time stats Recorded on every checkin; read them via snapshot().
Stack trace Captured at checkout time, so you can see exactly where it began.

Stats come from the engine's own pool: size(), checkedout(), overflow(), checkedin() — whichever the pool implementation exposes.

Compatible with

  • SQLAlchemy 2.0+
  • Any async driver: asyncpg, aiosqlite, aiomysql, asyncmy, aioodbc
  • Any pool class that fires checkout / checkin events (the default QueuePool / AsyncAdaptedQueuePool, StaticPool, SingletonThreadPool)
  • Sync Engine too — just attach(engine) works

Why this exists

Connection leaks and pool exhaustion are some of the hardest-to-debug production problems in async Python services. The pool runs out, requests pile up, and the stack trace points at code that's just waiting for a connection — not at the handler that forgot to release one.

sqla-pool-doctor records the stack at checkout, so when a leak warning fires, you see the real origin.

For deeper coverage of the underlying mechanics — pool sizing, overflow behaviour, leak patterns, and how to recover gracefully — see the companion guides on async engines and connection pooling:

License

MIT

About

Diagnose SQLAlchemy 2.0 async connection-pool leaks, exhaustion, and slow checkouts with one line of code — live stats, a JSON healthz snapshot, and stack traces to the real culprit.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages