Skip to content
rusthacks.dev
Databases

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.

2 min read
Summarize in AI
Flat illustration for the post “SQLx Connection Pools Sizing and Timeouts in Rust Explained”, composed around web.

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

rust
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
}
OptionWhy
max_connectionsthe cap per process
min_connectionskeeps a few warm so the first requests do not pay a handshake
acquire_timeouthow long a caller waits before getting PoolTimedOut
idle_timeoutcloses unused connections so a quiet service shrinks
max_lifetimerecycles 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

Text
pool timed out while waiting for an open connection

This 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 fetch stream 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.

Taggedpoolsqlx
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.