Configuring Puma Workers and Connection Pool Size in Rails
If you run Puma in clustered mode, your worker count, thread count, and database pool size all need to line up. Misconfigure them and you will either waste resources or hit connection errors under load. Here is how I size all three.
Workers
Puma in clustered mode spawns one worker process per core. Set the count from an environment variable so you can adjust it without code changes.
config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
On a 4-core machine dedicated to one Rails app, set WEB_CONCURRENCY=4. If two Rails apps share that machine, split the cores between them.
Threads
Each worker runs multiple threads to handle concurrent requests. The threads setting takes a min and a max.
config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
I set min and max to the same value. That avoids the overhead of Puma spinning threads up and down as load shifts. Five threads per worker is a reasonable starting point for most Rails apps.
Connection pool
Each thread needs its own database connection. Too small a pool and threads wait, then time out. Too large a pool and you exhaust the database server's connection limit.
The pool size should equal your thread count.
config/database.yml
production:
adapter: postgresql
url: <%= ENV["DATABASE_URL"] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
Reusing RAILS_MAX_THREADS for both keeps them in sync automatically.
Total connections
Multiply workers by threads per worker:
Total connections = workers x threads per worker
Four workers and five threads each is 20 connections per server. If you run multiple servers behind a load balancer, multiply again by the number of servers. I have seen teams scale horizontally and hit the database connection limit without realizing how the math compounded.
Sidekiq counts too. Default Sidekiq concurrency is 10, which is 10 additional connections per Sidekiq process.
Worked example
Two web servers with 4 cores and 5 threads per worker, plus one Sidekiq process:
Web: 2 servers x 4 workers x 5 threads = 40 connections
Sidekiq: 1 process x 10 concurrency = 10 connections
Total: 50 connections
PostgreSQL's default max_connections is 100, which leaves headroom. Scale to 4 web servers and you are at 90 — at that point either raise the database limit or put PgBouncer in front.
Full Puma config
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
preload_app!
preload_app! loads the application before forking workers. That cuts memory through copy-on-write and speeds up worker startup. Rails 5.2+ re-establishes database connections in the forked workers automatically, so you do not need an on_worker_boot block for that.
The takeaway: keep thread count and pool size tied to the same environment variable, and do the connection math before you scale.