The borrow checker and the database pool
This post was created with AI assistance and follows the Kladde 26 manifesto.
Kladde 26 is a manifesto for transparency in AI-assisted writing; it asks writers to show their workings. The frequently asked questions provide the background.
I have been writing Rust for a while now, and there is one thing I keep having to re-explain to myself: The borrow checker says that at any moment there is either one mutable reference to a value, or any number of read-only ones. Then I look at Rostfacto, my retrospective app that started as a port of Postfacto, where dozens of HTTP handlers write to the same PostgreSQL database at the same time. Those two facts do not seem to fit together.
Turns out, they do fit, but only once you separate three things that all get called "the database" in everyday speech. The rule the compiler enforces, the thing the pool actually shares, and the thing PostgreSQL does when two writes really do collide are not the same thing at all.
Let's walk through it with the actual code.
The rule is smaller than it sounds
The borrow checker rule is about one value, at one instant, on one thread. It says nothing about databases, and nothing about two functions that run at different times or on different threads. A &mut is exclusive only for its lifetime. When the function returns, the borrow is over, and the next function may take its own.
So the real question is not how many handlers can write to the database. It is how to arrange the code so that no two handlers ever hold a &mut to the same Rust value at the same time, while they all still talk to one database.
The answer has three layers, and then PostgreSQL does the rest.
A pool is a handle, not a connection
The first thing to unlearn is that state.pool is the database connection. It is a small handle to a pool of connections. In sqlx, PgPool is roughly an Arc around the pool's shared state. Cloning it does not copy a connection and does not duplicate the database. It bumps a reference count. You now have two separate Rust values that both point at the same internals.
That is why the borrow checker is happy. When handler A and handler B each get their own clone, the compiler sees two owned values. Nobody borrows the same value mutably, so there is nothing to reject.
In src/main.rs the state is built once and then cloned everywhere:
#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
// ...
}
It goes into the background tasks as pool.clone() and into the router with .with_state(state.clone()) (src/main.rs).
Interior mutability moves the check to runtime
If everyone has their own handle, who keeps the shared internals sane? The pool does, at runtime, with a lock or a semaphore. This is interior mutability: a value that looks immutable from the outside can still change its insides, because those insides sit behind a Mutex, an atomic, or a channel.
When a handler asks the pool for a connection, it gets a PoolConnection, a distinct physical connection checked out to that handler alone. Two concurrent handlers get two different connections. When the PoolConnection is dropped, it goes back into the pool.
The one-writer rule is still in force. It is just enforced by the pool's own locking instead of by the compiler. Rostfacto has a hand-rolled version of the same idea in EventHub (src/events.rs):
#[derive(Clone, Default)]
pub struct EventHub {
inner: Arc<EventHubInner>,
}
#[derive(Default)]
struct EventHubInner {
subscribers: Mutex<HashMap<i32, Vec<mpsc::UnboundedSender<Event>>>>,
}
EventHub is cloneable, and the map inside is guarded by a Mutex. publish takes &self, locks, mutates, unlocks. No &mut EventHub is ever needed, so any number of tasks can hold a clone and publish at the same time.
Every request gets its own clone
Axum's State extractor clones the state for each request. Every handler in handlers.rs starts like this:
pub async fn add_item(
State(state): State<AppState>,
// ...
) -> Result<Response, HandlerError> {
So when fifty requests hit add_item at once, there are fifty independent AppState values, each with its own pool handle, each running on the Tokio runtime, possibly on different operating system threads. No two of them borrow the same Rust value. AppState is Send + Sync + 'static, which is what lets axum move the clones across threads.
Inside the handler the write is a short, sequential borrow (src/handlers.rs):
let mut tx = state.pool.begin().await?;
let item_id = sqlx::query_scalar!(/* INSERT ... */)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
The &mut *tx is exclusive, but only for that one statement, and only to this handler's transaction. The next handler has its own transaction on its own connection. The borrows never overlap.
PostgreSQL does the rest
Even with all of that, two handlers can genuinely write to the same rows at the same time, on two different connections. Rust has nothing to do with that collision. It is a database problem, and PostgreSQL solves it with transactions, row locks, and MVCC.
Rostfacto leans on this on purpose. The single_highlighted_item_per_retro partial unique index makes "only one highlighted card" a database invariant, so two racing highlight requests cannot both win. One of them gets a constraint error. And emit_event writes the event row in the same transaction as the mutation, so the event log can never disagree with the data.
The picture
flowchart TD
A["main: PgPool::connect once"] --> B["AppState built once"]
B --> C["state.clone() per request"]
C --> D1["Handler A owns its clone"]
C --> D2["Handler B owns its clone"]
D1 --> E1["pool.acquire: connection 1"]
D2 --> E2["pool.acquire: connection 2"]
E1 --> F["PostgreSQL: transactions, locks, MVCC"]
E2 --> F
The thing I keep in my head now is this: The compiler never sees "the database". It sees PgPool values. Cloning a pool is a reference count bump, so each handler owns a separate value and there is no aliasing to complain about. The one-writer rule is enforced at runtime by the pool's lock and by PostgreSQL, not at compile time. And &mut exclusivity is per value and per lifetime, so a transaction borrow that lasts one statement is no obstacle to the next handler.
The rule I had in mind is still true; it is just that the shared thing is a handle whose insides are synchronized, not a single connection that everyone borrows.
The prompt I used
Help me understand something I always struggle with:
In Rust, the borrow checker enforces that at any point in time, only one function (?) can own a mutable reference to a variable. You can have unlimited read-only references to the same variable.
But in a web app like Rostfacto, we set up the database connection once, and then pass it to multiple HTTP handler functions in parallel.
How come these all seem to be able to write to the database?
Explain the fundamentals in high-level terms to me, and then show code examples from Rostfacto on how it's done in detail.