Connection Pools and State
Sharing database connections.
Learning Objectives
- Initialize a PgPool
##Why you never open a connection per request
Connecting to a database is slow: it involves a network handshake, authentication and setup. If every request opened its own connection, your server would spend more time connecting than working, and the database would quickly run out of connection slots.
A connection pool solves this. It opens a handful of connections once, keeps them alive and lends them to requests. When a request is done, the connection goes back to the pool for the next one.
#The pool lifecycle
- At startup the pool opens its minimum number of connections.
- A handler asks for a connection. If one is idle, it gets it instantly.
- If all are busy, the request waits (up to a timeout) until one is returned.
- The pool can grow up to a configured maximum.
#Creating a pool with SQLx
[dependencies]
actix-web = "4"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1", features = ["full"] }use actix_web::{web, App, HttpServer};
use sqlx::postgres::PgPoolOptions;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(10)
.acquire_timeout(std::time::Duration::from_secs(3))
.connect(&database_url)
.await
.expect("failed to connect to the database");
let pool = web::Data::new(pool);
HttpServer::new(move || App::new().app_data(pool.clone()))
.bind(("127.0.0.1", 8080))?
.run()
.await
}PgPool is already thread safe and cheap to clone, so you do not wrap it in a Mutex. web::Data shares the same pool with every worker.
#Using the pool in a handler
use actix_web::{get, web, HttpResponse};
use sqlx::PgPool;
#[get("/db-check")]
async fn db_check(pool: web::Data<PgPool>) -> HttpResponse {
match sqlx::query("SELECT 1").execute(pool.get_ref()).await {
Ok(_) => HttpResponse::Ok().body("database is up"),
Err(_) => HttpResponse::ServiceUnavailable().body("database is down"),
}
}Passing pool.get_ref() gives SQLx a &PgPool. It borrows a connection for the duration of the query and returns it automatically.
#Sizing the pool
- Too small and requests queue up waiting for a connection.
- Too large and you overwhelm the database, which has its own connection limit.
- A good starting point is 5 to 20 connections per application instance. Remember that instances multiply: ten servers with a pool of 20 means 200 connections.
#Common settings
max_connections: the upper limit.min_connections: connections kept warm.acquire_timeout: how long a request waits for a free connection before failing.idle_timeout: close connections unused for a while.
"What If?" Experiment
Learning by doing. Modify the code to see what happens.
See what happens when the pool is exhausted.
Run two slow queries at once with a pool of one connection.
The second request waits, then fails with a pool timed out error.
Explanation: With one connection the second request has to queue, and the acquire timeout turns the wait into an error.
Common Mistakes
Anticipate and avoid these frequent pitfalls when working with this concept.
Connecting inside the handler
It looks simple and works for one request at a time.
Explanation: Per-request connections are slow and will exhaust the database under load. Share one pool.
Pool size larger than the database allows
Multiple instances each opening many connections add up quickly.
Explanation: Postgres usually allows about 100 connections. Size pools so total connections stay under that limit.
Practical Exercise
Add a database health check
TaskCreate GET /health/db that runs SELECT 1 and returns 200 or 503.
Requirements
- Take web::Data<PgPool> in the handler
- Return 200 when the query succeeds
- Return 503 when it fails
Need a hint?
Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.
Real-World Application
Every production service runs a pool, usually with the size configurable through an environment variable, and exposes a database health check for load balancers and Kubernetes probes.
Key Takeaways
- Create one pool at startup and share it with web::Data.
- PgPool is thread safe and cheap to clone; do not wrap it in a Mutex.
- Tune max_connections and acquire_timeout to protect the database.
- Remember that every app instance has its own pool.
Finished reading?
Mark this lesson as done to track your progress and earn XP.