SQLx Connection Pools Sizing and Timeouts in Rust Explained
A pool that is too small queues requests and one that is too large exhausts the database. Learn how to size it from your server's limits, set timeouts and spot the symptoms.

More connections do not make a database faster. PostgreSQL's throughput peaks at a small multiple of its CPU cores, and past that the extra connections just queue inside the server instead of inside your pool. So pool sizing is about protecting the database and failing clearly, not about maximizing parallelism. It belongs to the operating side of the SQLx explained guide.
sqlx 0.8, PostgreSQL 17, Rust 1.90.
Configure the pool
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;
async fn pool(url: &str) -> Result<sqlx::PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(10)
.min_connections(2)
.acquire_timeout(Duration::from_secs(3))
.idle_timeout(Duration::from_secs(300))
.max_lifetime(Duration::from_secs(1800))
.connect(url)
.await
}| Option | Why |
|---|---|
max_connections | the cap per process |
min_connections | keeps a few warm so the first requests do not pay a handshake |
acquire_timeout | how long a caller waits before getting PoolTimedOut |
idle_timeout | closes unused connections so a quiet service shrinks |
max_lifetime | recycles connections, which helps with load balancers and memory growth |
The arithmetic
Suppose the database allows 100 connections and you reserve 10 for admin tools and migrations. Ninety are available. Six instances means 15 per instance; but during a rolling deploy old and new instances overlap, so plan for 9 instances' worth, which is 10 each. Write the budget down where the next person will see it.
Symptoms of exhaustion
pool timed out while waiting for an open connectionThis is sqlx::Error::PoolTimedOut. It means every connection was in use for longer than acquire_timeout. Look at who is holding them:
- A transaction kept open while the handler makes an HTTP call
- A query that is slow because of a missing index
- A
fetchstream that is not consumed to the end
Query SELECT state, count(*) FROM pg_stat_activity GROUP BY state and look for many idle in transaction sessions, the sign of a held transaction.
Do not hold a connection while idle
Acquire late, release early. query(...).execute(&pool) borrows a connection only for that statement. A pool.begin() holds one until commit or drop, as explained in SQLx transactions, so do slow non-database work before you begin.
When to use a pooler
With many small instances, such as serverless functions, the sum of pools exceeds the server limit. A pooler like PgBouncer multiplexes. In transaction pooling mode session state (prepared statements by name, SET, advisory locks) does not survive between statements, so test with SQLx's statement cache disabled if you see prepared statement errors.
Measure
Expose pool.size() and pool.num_idle() as metrics, along with wait time, next to your other service metrics. A pool that is always full is the signal to look at query time first, and pool size second. The options are documented at PoolOptions.


