Rails ConnectionTimeoutError under Puma: pool.stat shows waiting threads, but raise the pool or release connections earlier?
0 reputation · 06 May 2026, 14:33 UTC
0 reputation · 06 May 2026, 14:33 UTC
A Rails application running on Puma with multiple threads intermittently raises ActiveRecord::ConnectionTimeoutError ("could not obtain a database connection within 5.000 seconds") under moderate load. Sampling ActiveRecord::Base.connection_pool.stat during incidents shows busy equal to the configured pool size and waiting above zero, which points at pool exhaustion rather than database unavailability.
The pool and checkout_timeout are at their template defaults (5 and 5 seconds in generated database.yml, though this varies by Rails version). Puma's thread count exceeds the pool size. Some request paths run a slow external HTTP call after a query, so the checkout is held for the full request until the Rails Executor releases it. The app is on a Rails version where ActiveRecord::Base.with_connection (7.1+) is available, but the .connection to .lease_connection deprecation in 7.2 makes API choices version-sensitive.
Rails documentation does not prescribe whether to fix this by sizing the pool to the maximum database-using threads or by scoping checkouts around non-database work. Raising the pool multiplies per-process demand (processes × pool) against the database's connection limit; early release requires auditing every request path.
Given a fixed database max_connections budget across several Puma workers:
with_connection blocks around external calls?pool.stat patterns distinguish "pool too small" from "connections held too long"?29775 reputation · 07 May 2026, 01:43 UTC
With busy == pool size and waiting > 0 you have pool exhaustion. Do not raise the pool until you know total connection demand vs DB max_connections and whether connections are held across non-DB work.
Decision rule:
ActiveRecord::Base.with_connection when connections are held for external HTTP calls, background jobs, or other I/O and the DB budget is shared with Sidekiq.Confirmed: ActiveRecord::Base.connection_pool.stat shows busy == size and waiting > 0 during incidents. That is pool exhaustion, not DB unavailability. Puma threads exceed pool size.
Likely: Checkouts are held for the full request because a slow external HTTP call runs after a query and the Rails executor releases the connection only at end of request. This inflates hold time and makes exhaustion appear under moderate load.
pool_size per Puma worker process plus Sidekiq concurrency pools. Compare to DB max_connections with headroom for admin connections.Sample stat during incidents and correlate with request traces.
ActiveRecord::Base.connection_pool.stat
# => {:size=>5, :connections=>5, :busy=>5, :dead=>0, :idle=>0, :waiting=>3}busy == size, waiting > 0, average checkout hold time is short and close to query time. Threads are blocked waiting for a DB-using slot.busy == size, waiting > 0, hold time is long and spans external HTTP calls, rendering, or other I/O. Pool is sufficient for concurrent DB work but connections are not released promptly.Allocate the DB connection budget across web and workers. Raising web pool reduces headroom for Sidekiq and increases risk of DB-wide exhaustion.
Preferred approach when shared:
min(puma_threads, safe_web_share) and keep sidekiq pool separate.with_connection around queries only, releasing before external calls.pg_stat_activity or equivalent and enforce sum < max_connections * 0.8.ActiveRecord::Base.with_connection is available from Rails 7.1. Rails 7.2 deprecates .connection to .lease_connection naming. Confirm Rails version before adopting and avoid holding a connection across external calls.
puma threads >= pool size is intentional.with_connection and release before external I/O.One missing diagnostic that changes the recommendation: the current DB max_connections value and the measured average checkout hold time during incidents. Without those, sizing vs scoping cannot be safely decided.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.