Introduction
Every database query requires a connection. Rails manages a pool of reusable connections, but misconfiguration can lead to timeouts, connection exhaustion, and degraded performance under load.
Key Concepts
- Connection Pool: A fixed set of reusable database connections shared across threads in a Rails process.
- Pool Size: The maximum number of concurrent connections a single Rails process can hold. Must match your thread count.
- PgBouncer: An external connection pooler that sits between Rails and PostgreSQL, multiplexing many application connections onto fewer database connections.
Real World Context
A Puma server with 3 workers and 5 threads each needs 15 database connections minimum. With Sidekiq running 25 threads, you need 25 more. Without proper pooling, you'll hit ActiveRecord::ConnectionTimeoutError under load.
Deep Dive
Pool Configuration
yaml# config/database.yml production: adapter: postgresql pool: <%= ENV.fetch('RAILS_MAX_THREADS', 5) %> url: <%= ENV['DATABASE_URL'] %> prepared_statements: true
Puma Thread/Pool Alignment
ruby# config/puma.rb workers ENV.fetch('WEB_CONCURRENCY', 2) threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) threads threads_count, threads_count preload_app! on_worker_boot do ActiveRecord::Base.establish_connection end
Each Puma worker forks a new process. preload_app! loads the app once, then each worker re-establishes its own connection pool.
Connection Checkout Timeout
yamlproduction: pool: 5 checkout_timeout: 5 # Wait 5 seconds for a connection (default)
If all connections are busy for longer than checkout_timeout, Rails raises ConnectionTimeoutError.
Using PgBouncer
When running many processes (web + workers + jobs), PgBouncer reduces the actual database connection count:
yaml# config/database.yml (with PgBouncer) production: url: <%= ENV['DATABASE_URL'] %> # Points to PgBouncer prepared_statements: false # Required with PgBouncer in transaction mode advisory_locks: false # Required with PgBouncer
Common Pitfalls
- Pool size smaller than thread count — If your pool is 5 but Puma runs 10 threads, 5 threads will wait for connections on every request.
- Forgetting to re-establish connections after fork — Puma workers share the parent's connections after forking. Always call
establish_connectioninon_worker_boot.
Best Practices
- Match pool size to thread count — Set
poolequal toRAILS_MAX_THREADSfor both Puma and Sidekiq. - Monitor connection usage — Track active connections with
ActiveRecord::Base.connection_pool.statto detect exhaustion before it causes errors.
Summary
- Set database pool size equal to your thread count per process.
- Re-establish connections after Puma worker forks with
on_worker_boot. - Use PgBouncer when running many processes to reduce database connection count.
- Monitor pool stats to detect exhaustion early.
Code Examples
ruby
# Check connection pool stats in Rails console
stats = ActiveRecord::Base.connection_pool.stat
puts stats
# => { size: 5, connections: 3, busy: 1, dead: 0,
# idle: 2, waiting: 0, checkout_timeout: 5.0 }
# size: pool max, busy: in-use, waiting: threads waiting for a connection