How to use connection pooling

Use the r2d2 crate with Diesel to create a Pool that manages reusable database connections for better performance.

The cost of a handshake

You are building a web service. A user hits an endpoint. Your code opens a database connection, runs a query, closes the connection, and sends the response. It works fine for one user. Then ten users hit the endpoint. Then a hundred. Your app grinds to a halt.

The database isn't the bottleneck. The handshake overhead is. Every time you open a connection, you pay for TCP negotiation, TLS setup, authentication, and protocol initialization. Doing that per-request is a performance trap. You are spending more time setting up the conversation than having it.

Connection pooling solves this by reusing connections. Instead of opening and closing a connection for every request, you maintain a set of open connections. When a request arrives, you grab a connection from the set. When the request finishes, you return the connection. The connection stays alive, authenticated, and ready. The next request grabs it instantly.

Connection pooling is a rental lot

Think of a connection pool like a rental car lot. You don't manufacture a new car every time someone needs to drive. You keep a fleet of cars parked and ready. When a customer arrives, they grab a car from the lot. When they are done, they return the car. The car stays in the lot, engine warm, keys in the ignition. The next customer grabs it without waiting for the factory.

In Rust, the r2d2 crate is the standard tool for this. It manages the fleet. It handles creating new connections, testing if a connection is still alive, and cleaning up broken ones. You just ask for a connection, use it, and let it go. The pool handles the rest.

Minimal setup

Add diesel and r2d2 to your Cargo.toml. The diesel crate re-exports r2d2 for convenience, so you can use diesel::r2d2 if you prefer. The crate name is r2d2.

/// Demonstrates basic pool creation and connection retrieval.
use diesel::r2d2::{self, ConnectionManager, PooledConnection};
use diesel::SqliteConnection;

fn main() {
    // The manager knows how to create, test, and destroy connections.
    // It holds the database URL and driver logic.
    let manager = ConnectionManager::<SqliteConnection>::new("file:mydb.sqlite");

    // The builder lets you tune pool settings before creation.
    // build() returns a Result; unwrap is fine for examples.
    let pool = r2d2::Pool::builder().build(manager).unwrap();

    // get() checks out a connection from the pool.
    // It returns a PooledConnection guard.
    // The connection returns to the pool when `conn` drops.
    let mut conn = pool.get().unwrap();

    println!("Got a connection from the pool!");
}

Convention aside: use r2d2::Pool::builder() to create pools. The builder pattern is the community standard because it makes configuration explicit. Direct construction is rarely used.

The pool owns the connections. You only ever borrow them. Never try to hold a connection across a long-running task without returning it, or you will starve the pool.

The manager and the guard

The pool relies on two key types: the manager and the guard.

The manager implements the ManageConnection trait. It tells the pool how to create a connection, how to check if a connection is valid, and how to detect if a connection has broken. For Diesel, ConnectionManager handles this. It knows how to open a SQLite, PostgreSQL, or MySQL connection. It also knows how to run a lightweight check to ensure the connection hasn't been dropped by the server.

The guard is the PooledConnection wrapper. When you call pool.get(), you don't get a raw connection. You get a guard. The guard holds a reference to the pool and the connection. When the guard goes out of scope, it automatically returns the connection to the pool. This is a Rust idiom. The guard ensures the connection is returned even if your code panics. You never have to manually call a "return" method.

/// Shows the guard behavior and automatic return.
use diesel::r2d2::{self, ConnectionManager};
use diesel::SqliteConnection;

fn main() {
    let manager = ConnectionManager::<SqliteConnection>::new("file:mydb.sqlite");
    let pool = r2d2::Pool::builder().build(manager).unwrap();

    // conn is a guard.
    // It borrows a connection from the pool.
    {
        let mut conn = pool.get().unwrap();
        // Use conn here.
        // The connection is checked out.
    }
    // conn drops here.
    // The connection returns to the pool automatically.
    // The pool can run validity checks before reusing it.
}

The guard also implements Deref and DerefMut. You can use the guard like a normal connection. Diesel's query methods work directly on the guard. You don't need to unwrap the connection.

Trust the guard. It handles the lifecycle. If you try to extract the raw connection, you break the pool's tracking. Stick to the guard.

Realistic application state

In a real application, you need to share the pool across handlers, background tasks, and middleware. You cannot move the pool into multiple places. You need shared ownership.

The standard pattern is to wrap the pool in Arc. Arc provides thread-safe shared ownership. You create the pool once, wrap it in Arc, and clone the Arc to pass it around. Cloning the Arc is cheap. It just increments a reference count. It does not clone the pool or the connections.

/// Realistic pool setup with shared state.
use std::sync::Arc;
use diesel::r2d2::{self, ConnectionManager, Pool};
use diesel::SqliteConnection;

// Type alias for readability.
type DbPool = Pool<ConnectionManager<SqliteConnection>>;

/// Application state shared across handlers.
struct AppState {
    pool: Arc<DbPool>,
}

fn create_pool() -> DbPool {
    let manager = ConnectionManager::<SqliteConnection>::new("file:mydb.sqlite");

    // Configure pool limits.
    // max_size prevents overwhelming the database.
    // min_idle keeps connections warm for low traffic.
    r2d2::Pool::builder()
        .max_size(10)
        .min_idle(Some(5))
        .build(manager)
        .expect("Failed to create pool")
}

fn main() {
    let pool = create_pool();
    let state = AppState {
        // Wrap in Arc for sharing.
        pool: Arc::new(pool),
    };

    // Clone the Arc to pass to handlers.
    // This is the convention: explicit Arc::clone.
    let handler_pool = Arc::clone(&state.pool);
    spawn_handler(handler_pool);
}

fn spawn_handler(pool: Arc<DbPool>) {
    // In a real app, this runs in a thread or async task.
    let mut conn = pool.get().expect("Pool error");
    // Use conn...
}

Convention aside: use Arc::clone(&pool) instead of pool.clone(). Both compile and work. The explicit form signals to readers that you are cloning the reference count, not the pool. It avoids confusion with deep clones.

Pool configuration matters. max_size limits concurrent connections. Set this based on your database's limits. min_idle keeps a minimum number of connections open. This reduces latency during traffic spikes. connection_timeout controls how long get() waits if the pool is exhausted. Tune these values based on your load.

Wrap your pool in Arc and share that. Cloning the Arc is the only way to pass the pool around without fighting the borrow checker or creating duplicate pools.

Pitfalls and errors

Connection pooling introduces new failure modes. Understanding them prevents production incidents.

Pool exhaustion happens when all connections are checked out and a new request calls get(). The pool blocks until a connection is returned or the timeout expires. If the timeout expires, get() returns an error. If you unwrap() the error, your app panics. In production, handle the error gracefully. Return a 503 or retry with backoff.

If you try to move the pool into two different handlers, the compiler rejects you with E0382 (use of moved value). The pool has one owner. Use Arc to share ownership. If you try to share a pool across threads without Arc, the compiler rejects you with E0277 (trait bound not satisfied) because you need shared ownership semantics. Arc provides Send and Sync for the pool.

Starvation occurs when a handler holds a connection too long. If a handler runs a slow query or blocks on I/O while holding a connection, it ties up a slot in the pool. Other requests wait. Monitor your query times. Use timeouts. Return connections quickly.

Broken connections can happen if the database server restarts or the network drops. The pool detects this via the manager's is_valid and has_broken checks. When a connection is returned, the pool runs these checks. If the connection is broken, the pool discards it and creates a new one. This is transparent to your code. You might get a new connection on the next get(). The pool handles recovery.

Pool exhaustion is silent until it's not. Monitor your pool size and timeout settings. If get() starts failing, your app is waiting on the database, not the database itself.

Choosing a pool

Rust has several pooling crates. Pick the right one for your stack.

Use r2d2 when you need a battle-tested, synchronous pool for Diesel or other synchronous drivers. It is the standard for synchronous Rust. It has a large ecosystem and stable API.

Use bb8 when you are building an async application with tokio and need an async-aware pool. bb8 provides async get() and integrates with async runtimes. It is a modern choice for async stacks.

Use deadpool when you want a generic async pool with extensive configuration and runtime metrics. It supports multiple runtimes and provides detailed pool statistics.

Reach for raw connections when you are writing a script, a migration tool, or a test where pool overhead is unnecessary. Pooling adds complexity. If you only have one connection, skip the pool.

Async apps need async pools. Don't block your event loop with a synchronous pool.

Where to go next