← Back to Blog

Configuring Puma Workers and Connection Pool Size in Rails

Antarr Byrd, Rails consultant and founder of Ruby Roost
Antarr Byrd·2026-03-04
Ruby on RailsPumaPostgreSQLPerformance
Share

When deploying a Ruby on Rails application with Puma in clustered mode, one of the first things you need to get right is the relationship between your worker count, thread count, and database connection pool. If these are misconfigured you will either waste resources or run into connection errors under load.

I recently went through this process while tuning our deployment and wanted to share the approach I used.

Setting Workers

Puma in clustered mode spawns multiple worker processes. The general rule is one worker per CPU core available to your application.

config/puma.rb

workers ENV.fetch("WEB_CONCURRENCY") { 2 }

If you are running on a 4-core machine dedicated to a single Rails app, set WEB_CONCURRENCY=4. If you are running two Rails apps on that same machine, split the cores between them. I set this as an environment variable so it can be adjusted per environment without changing code.

Setting Threads

Each worker runs multiple threads to handle concurrent requests. The threads setting takes a min and max value.

config/puma.rb

threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count

I set the min and max to the same value. This avoids the overhead of Puma spinning threads up and down under varying load. For most Rails applications 5 threads per worker is a reasonable starting point.

Sizing the Connection Pool

This is where things can go wrong. Each thread needs its own database connection. If your pool is too small, threads will wait for a connection and eventually timeout. If it is too large you will exhaust your database server's connection limit.

The rule is straightforward. Your connection pool size should equal your thread count.

config/database.yml

production:
  adapter: postgresql
  url: <%= ENV["DATABASE_URL"] %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

By using the same RAILS_MAX_THREADS variable for both Puma threads and the connection pool, they stay in sync automatically.

Calculating Total Connections

To figure out how many database connections your application will use, multiply workers by threads:

Total connections = workers x threads per worker

If you have 4 workers and 5 threads each, that is 20 database connections from a single server. If you are running multiple servers behind a load balancer, multiply again by the number of servers. I have seen this catch people off guard when they scale horizontally and suddenly hit the database connection limit.

If you are using Sidekiq or other background job processors on the same database, those connections count as well. Sidekiq's default concurrency is 10, which means 10 additional connections per Sidekiq process.

An Example Configuration

For a deployment with 2 servers, each 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, so this leaves headroom. But if you scale to 4 web servers you are at 90 connections and should either increase the database limit or add a connection pooler like PgBouncer.

Putting It Together

Here is the full Puma configuration I use:

config/puma.rb

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!

The preload_app! directive loads the application before forking workers. This reduces memory usage through copy-on-write and speeds up worker startup. Rails will automatically re-establish database connections in the forked workers so you do not need an on_worker_boot block for that in Rails 5.2 and later.

The key takeaway is to keep your thread count and pool size in sync using the same environment variable, and to calculate your total connection usage before scaling.

Please share any feedback to improve this or other postings.

Share