Date: 2026-07-27 Status: Accepted
The backend maintains two categories of pooled connections that were added incrementally without a recorded decision:
- A PostgreSQL connection pool (
backend/src/db/database.ts), used whenDATABASE_URLpoints at Postgres (staging/prod), created via thepgdriver'sPoolwithmax: 10. - A Soroban RPC connection pool (
backend/src/utils/connectionPool.ts), used for all calls to the Soroban JSON-RPC endpoint, with configurablemin/maxsize, retry-with-backoff, and pool exhaustion handling. Its metrics are exposed under thedb_pool_*names (dbPoolAcquiredTotal,dbPoolAvailable,dbPoolWaitMs).
SQLite itself (the default local/dev driver, see ADR-003) does not use a network-style connection pool: it is a single embedded file accessed synchronously in-process, so there is no connection handshake to amortize.
- Do not pool SQLite connections. The SQLite driver opens a single in-process handle; pooling would add complexity without a corresponding benefit for a single-writer, file-backed database.
- Pool PostgreSQL connections (via
pg.Pool) because Postgres is used in staging/production, is accessed over the network, and benefits from reusing established connections under concurrent requests. - Pool Soroban RPC connections because each RPC call is a network round trip to the Stellar network, and the pool bounds concurrent outstanding requests while allowing burst capacity up to
max.
| Option | Reason not chosen |
|---|---|
| Pool SQLite handles like network connections | SQLite access is in-process and synchronous; pooling adds overhead and lock contention risk with no throughput benefit |
| Single shared RPC client with no pool | Under load, a single client serializes RPC calls and has no backpressure/exhaustion signal; the pool gives bounded concurrency and retry semantics |
| Unbounded RPC client creation per request | Risks exhausting file descriptors / RPC provider rate limits under load |
Positive:
- Postgres and Soroban RPC usage scale under concurrent load without per-call connection setup cost.
- Pool exhaustion is an explicit, observable failure (
PoolExhaustedError) rather than unbounded resource growth. - SQLite path stays simple, matching its intended use as the zero-dependency local/dev database.
Negative / Trade-offs:
- Two different pooling implementations exist (
pg.Poolfor Postgres, a custom pool for Soroban RPC) rather than one shared abstraction. - If SQLite is ever used under high write concurrency in production, its lack of pooling/queueing could become a bottleneck β see ADR-003 for the scaling trade-off already accepted there.