Skip to content

Puma

Web & HTTPWeb / HTTP ServerRuby

What it is

Puma is a concurrent HTTP server for Ruby, using a thread pool and optional worker processes, and is the default server for Rails.

Workers are processes, threads are concurrency within a worker. Correct sizing depends on available memory and the database connection pool.

Installation

gem 'puma'

Getting started

The smallest useful thing you can do with it, and what each part means.

Production configuration
# config/puma.rb
max_threads = ENV.fetch('RAILS_MAX_THREADS', 5).to_i
threads max_threads, max_threads

workers ENV.fetch('WEB_CONCURRENCY', 2).to_i
preload_app!                   # required for copy-on-write memory savings

port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'production')

# Each forked worker needs its own connections.
on_worker_boot do
  ActiveRecord::Base.establish_connection
end

before_fork do
  ActiveRecord::Base.connection_pool.disconnect!
end

plugin :tmp_restart
Forking without reconnecting is a classic failure: children inherit the parent's sockets and corrupt each other's traffic. on_worker_boot is not optional with preload_app!.

Advanced usage

Where the library earns its place over a simpler alternative.

Sizing and graceful restarts
# Total concurrency = workers x threads.
# The database pool must cover the threads in ONE worker:
#   pool: <%= ENV.fetch('RAILS_MAX_THREADS', 5) %>

# MRI has a GIL, so threads help with I/O-bound work, not CPU-bound.
# Start with threads 5 and workers = cores, then measure.

# Phased restart — zero downtime, workers replaced one at a time.
#   pumactl phased-restart
# Note: it does not reload preloaded code, so it is unsuitable for
# deploys that change the application itself.

worker_timeout 60   # kill a worker that stops responding
The GIL is why thread count has limits: Ruby threads interleave on I/O but not on computation, so a CPU-heavy endpoint needs more workers rather than more threads.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

PG::ConnectionBad or corrupted responses after forking
Connections were inherited across the fork. Disconnect in before_fork and reconnect in on_worker_boot.
Requests queue while CPU is idle
Threads are blocked on the connection pool. Raise the pool to match RAILS_MAX_THREADS.

Best practices

  • Set the database pool to at least the thread count per worker.
  • Use preload_app! with on_worker_boot to reconnect, or workers will share sockets.
  • Size workers by memory and threads by I/O profile; measure rather than guessing.
  • Set worker_timeout so a hung worker is replaced rather than blocking traffic.

Background

Why it exists, and what it was reacting to.

Puma replaced older single-threaded servers by using threads within workers, which suits Ruby's I/O-bound web workloads far better than one process per request.