Skip to content

Database Connection Pool

Loquent uses a single shared SeaORM connection pool per process. The pool is initialized lazily on first use and shared across all requests, services, cron jobs, and webhooks.

Call db_client() from anywhere in server-side code. On the first call, it builds a DatabaseConnection pool from the DATABASE_URL environment variable and stores it in a tokio::sync::OnceCell. Every subsequent call returns a cheap Arc clone of the same pool.

use crate::bases::db::db_client;
let db = db_client().await?;
// Use `db` for queries — it's a shared pool clone.

The pool is configured with these defaults in src/bases/db/services/db_client_service.rs:

SettingValuePurpose
max_connections20Maximum concurrent Postgres connections
min_connections1Keep at least one connection warm
idle_timeout120sDrop idle connections before Railway’s proxy kills them (~300s)
max_lifetime600sRecycle connections to prevent stale state
acquire_timeout5sFail fast if pool is exhausted
connect_timeout5sFail fast on unreachable database

The singleton lives in src/bases/db/services/db_client_service.rs:

use tokio::sync::OnceCell;
use sea_orm::DatabaseConnection;
static POOL: OnceCell<DatabaseConnection> = OnceCell::const_new();
pub async fn db_client() -> Result<DatabaseConnection, AppError> {
let pool = POOL
.get_or_try_init(|| async {
let db_url = std::env::var("DATABASE_URL")?;
let mut opt = ConnectOptions::new(db_url);
opt.sqlx_logging(false)
.idle_timeout(Duration::from_secs(120))
.max_lifetime(Duration::from_secs(600))
.acquire_timeout(Duration::from_secs(5))
.connect_timeout(Duration::from_secs(5))
.max_connections(20)
.min_connections(1);
let db = Database::connect(opt).await?;
tracing::info!("Database connection pool initialized");
Ok(db)
})
.await?;
Ok(pool.clone())
}

Key details:

  • OnceCell::const_new() creates the cell at compile time with zero runtime cost.
  • get_or_try_init runs the initialization closure exactly once. Concurrent callers wait on the same future.
  • pool.clone() is cheap — DatabaseConnection wraps Arc<sqlx::Pool<Postgres>>, so cloning increments a reference count.
  • sqlx_logging(false) disables per-query SQL logging to keep logs clean.

On SIGTERM or Ctrl-C, the shutdown handler in src/main.rs closes the pool cleanly:

match db::db_client().await {
Ok(pool) => pool.close().await.unwrap_or_else(|e| {
tracing::warn!(error = %e, "Failed to close DB pool cleanly");
}),
Err(e) => {
tracing::warn!(error = %e, "Failed to obtain DB pool for shutdown close");
}
}

This sends a Postgres Terminate message on each connection instead of dropping TCP connections with a RST — preventing Connection reset by peer errors in Postgres logs.

VariableRequiredDescription
DATABASE_URLYesPostgres connection string (e.g., postgres://user:pass@host:5432/loquent)

“Database connection pool initialized” appears more than once — This should never happen. If it does, check that db_client() is being imported from crate::bases::db and not re-implemented elsewhere.

Connection timeouts under load — The pool caps at 20 connections. If you consistently hit acquire_timeout, consider increasing max_connections — but check Postgres max_connections first (Railway default: 100).