Diagnosing and Fixing ActiveRecord Connection Pool Exhaustion in Rails
A step‑by‑step diagnostic guide for ActiveRecord connection‑pool exhaustion in Rails, covering symptoms, causes, checks, fixes, and when to involve a DBA.
07 Oct 2026, 17:17 UTC

Recognizable Condition
Requests start failing with ActiveRecord::ConnectionNotEstablished or the server logs show could not obtain a database connection within 5 seconds. Latency spikes and the request queue grows while the CPU remains idle.
Cause–Diagnostic Table
| Observed Symptom | Typical Cause | Quick Verification |
|---|---|---|
Intermittent ConnectionNotEstablished under load | Pool size too small for concurrent threads/processes | Run rails runner 'puts ActiveRecord::Base.connection_pool.stat' and compare size vs checkout |
| Steady increase of checked‑out connections | Long‑running or unoptimized queries | Enable config.active_record.verbose_query_logs = true and watch slow‑query logs |
| Connections never return to pool | Transaction leaks or missing with_connection blocks | Add middleware logging checkout/checkin timestamps |
Database reports max_connections reached | Sum of per‑process pools exceeds DB limit | Run SHOW VARIABLES LIKE 'max_connections'; (MySQL) or SHOW max_connections; (PostgreSQL) |
Ordered Checks
- Confirm configured pool size. Open
config/database.ymland note thepoolvalue for the current environment. In a multi‑process server (Puma workers, Unicorn, Sidekiq) each process gets its own pool. - Measure actual checkout/checkin rates. Run the runner command above while the app is under realistic load (e.g.,
ab -n 1000 -c 20 http://localhost:3000/). The output showssize,connections,busy,dead. Ifbusy==sizefrequently, the pool is saturated. - Identify long‑running queries. Enable
config.active_record.verbose_query_logs = truetemporarily and taillog/production.logfor queries exceeding thecheckout_timeout(default 5 s). - Detect connection leaks. Insert a small Rack middleware that logs
ActiveRecord::Base.connection_pool.checkoutandcheckinwith timestamps. A growing gap indicates a leak. - Validate database server limits. Execute the
SHOW VARIABLEScommand on the DB host. Multiplypoolby the number of Rails processes; the product must stay well belowmax_connections(leave ~20 % headroom for other clients).
Fixes Tied to Findings
- Pool too small – Increase
poolindatabase.yml(e.g.,pool: 15) and restart the app servers. Verify with the runner command thatsizereflects the new value. - Long‑running queries – Add appropriate indexes, rewrite N+1 queries, or move heavy work to background jobs. After changes, re‑run the load test and confirm
busydrops. - Leaked connections – Ensure every manual checkout uses
ActiveRecord::Base.connection_pool.with_connection { … }or explicitly callscheckin. Wrap Rake tasks and custom threads with the same pattern. - Per‑process pool exceeds DB limit – Reduce
poolper process or consolidate workers (e.g., fewer Puma workers, more threads). Alternatively, raise the DBmax_connectionssetting (requires DB restart) and monitor memory usage.
Escalation Criteria
Escalate to infrastructure or DBA when:
- After applying the above fixes,
busystill equalssizeunder normal traffic. - The database server hits
max_connectionseven though the calculated total pool is below the limit (indicates external consumers). - Connection latency remains > 200 ms after pool tuning, suggesting network or DB‑engine contention.
At that point consider a connection proxy such as PgBouncer (PostgreSQL) or ProxySQL (MySQL), or move to a read‑replica architecture.
Limitations & Verification
This guide covers the built‑in ActiveRecord pool only. External poolers, fork‑safe configurations, and Rails 7+ connection_handling changes may behave differently. Always verify changes in a staging environment that mirrors production process count and traffic pattern before rolling out.
Practical verification: After each fix, run the runner command again and confirm busy stays comfortably below size (target busy/size < 0.7) during a 5‑minute load test.