Skip to content

Pool metrics: the timeout counter never counts pool checkout timeouts, and the checkout histogram measures held time, not wait #26

Description

@AlexeyShalaev

Measured on 0.3.0 with PostgresMetrics() attached to an AsyncSessionManager with PoolSettings(size=2, max_overflow=2, timeout=1.0), eight workers running SELECT pg_sleep(:s) in a loop, sampled every half second from the Prometheus registry next to the application's own count of sqlalchemy.exc.TimeoutError raised by the pool:

   3.0 s  in_use=4/4  checkouts/s=  72  checkout wait=   55 ms  timeouts_total=0 errors_total=0  requests failed on pool timeout=0
  ... the database slows down: queries take 1.0 s instead of 0.05 s
   5.0 s  in_use=4/4  checkouts/s=   8  checkout wait= 1007 ms  timeouts_total=0 errors_total=0  requests failed on pool timeout=3
   7.0 s  in_use=4/4  checkouts/s=   8  checkout wait= 1006 ms  timeouts_total=0 errors_total=0  requests failed on pool timeout=6
   8.0 s  in_use=4/4  checkouts/s=   8  checkout wait= 1007 ms  timeouts_total=0 errors_total=0  requests failed on pool timeout=7

Two things in that table.

postgres_db_connection_timeouts_total is documented as "Number of connection checkout timeouts" and it stayed at zero through seven of them. attach_metrics counts timeouts from the engine's handle_error event, which fires for DBAPI errors during execution; a QueuePool checkout timeout is raised by pool.connect() before any DBAPI call and never reaches that listener. So the one timeout a pool actually produces under load is the one the counter does not count.

postgres_db_connection_checkout_duration_seconds is the time between the checkout and checkin events, which is how long a connection was held, and the column above confirms it: 55 ms while queries took 50 ms, a second while they took a second. That is a useful number (it is the query duration seen from the pool), but its name says checkout, the guide's monitoring advice reads it as the time a caller waited for a connection, and the wait is the metric that predicts a pool outage. The wait is not exposed at all.

What I think it needs: a real checkout-wait histogram (time spent inside pool.connect() before a connection is handed out) and a timeout counter that increments when that wait ends in TimeoutError. SQLAlchemy has no pool event for "checkout requested", so the instrumentation has to sit around the acquisition: the kit already owns the pool class through resolve_pool_class / register_pool_class, and an instrumented AsyncAdaptedQueuePool subclass that times _do_get and counts its TimeoutError would cover both without touching callers; timing session.connection() in get_session would be the alternative. The existing histogram should keep its data and get a name that says what it measures (held duration), with a deprecation note in the agents page's metrics paragraph, the monitoring section of the configuration guide, and the PgBouncer guide, which quotes it.

Lab: pool_metrics_lab.py in https://github.com/bedrock-python/bedrock-python.github.io/tree/docs/production-python-series/docs/blog/lab/2026-09-07-sqlalchemy-pool-metrics.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions