rust 114 lines · 3 tabs

Building a Bounded Async Database Connection Pool With Tokio Semaphore

Shared by codesnips Aug 2026
3 tabs
use async_trait::async_trait;
use std::io;

#[async_trait]
pub trait ConnectionFactory: Send + Sync + 'static {
    type Connection: Send + 'static;

    async fn connect(&self) -> io::Result<Self::Connection>;
}

pub struct TcpFactory {
    pub addr: String,
}

#[async_trait]
impl ConnectionFactory for TcpFactory {
    type Connection = tokio::net::TcpStream;

    async fn connect(&self) -> io::Result<Self::Connection> {
        tokio::net::TcpStream::connect(&self.addr).await
    }
}
3 files · rust Explain with highlit

This snippet shows a small, self-contained async connection pool built on tokio, the kind of primitive that sits under higher-level database crates. The core idea is that a pool bounds the number of live connections and hands them out one at a time, blocking (asynchronously) when the pool is exhausted so callers naturally experience backpressure instead of opening an unbounded number of sockets.

In pool.rs, the Pool owns a Semaphore sized to max_size and a Mutex<Vec<C>> of idle connections. The semaphore is the real gatekeeper: acquire first takes a permit via acquire_owned, and only once a permit is held does it try to pop an idle connection or build a fresh one through the ConnectionFactory. Because the permit is owned (OwnedSemaphorePermit), it can live inside the guard for as long as the caller holds the connection, which is what keeps the in-flight count correct. The permit count and the vector of idle connections stay in sync without any spin-looping.

The returned PooledConnection in guard.rs is an RAII handle. It derefs to the underlying connection so callers use it transparently, and its Drop implementation returns the connection to the idle list and drops the permit, freeing a slot for the next waiter. The try_lock fallback in Drop matters: Drop cannot be async, so it uses the blocking lock and simply discards the connection if the mutex is momentarily contended, which is safe because losing a pooled connection only forces a later rebuild.

The ConnectionFactory trait in factory.rs decouples the pool from any specific driver; a Postgres, Redis, or mock factory all implement the same async connect. This is the extension point that makes the pool reusable.

The main trade-off is simplicity over features: there is no health-checking, idle timeout, or connection aging here, so a broken connection could be reused. The design assumes callers hold guards briefly. It fits cases where an app needs strict concurrency limits and predictable resource usage without pulling in a full pooling library.


Related snips

Share this code

Here's the card — post it anywhere.

Building a Bounded Async Database Connection Pool With Tokio Semaphore — share card
Link copied