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.
How it works
Section titled “How it works”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.Pool configuration
Section titled “Pool configuration”The pool is configured with these defaults in src/bases/db/services/db_client_service.rs:
| Setting | Value | Purpose |
|---|---|---|
max_connections | 20 | Maximum concurrent Postgres connections |
min_connections | 1 | Keep at least one connection warm |
idle_timeout | 120s | Drop idle connections before Railway’s proxy kills them (~300s) |
max_lifetime | 600s | Recycle connections to prevent stale state |
acquire_timeout | 5s | Fail fast if pool is exhausted |
connect_timeout | 5s | Fail fast on unreachable database |
Implementation
Section titled “Implementation”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_initruns the initialization closure exactly once. Concurrent callers wait on the same future.pool.clone()is cheap —DatabaseConnectionwrapsArc<sqlx::Pool<Postgres>>, so cloning increments a reference count.sqlx_logging(false)disables per-query SQL logging to keep logs clean.
Shutdown
Section titled “Shutdown”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.
Environment variables
Section titled “Environment variables”| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Postgres connection string (e.g., postgres://user:pass@host:5432/loquent) |
Troubleshooting
Section titled “Troubleshooting”“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).