Skip to main content

Puma Overview

Puma is the web server that receives http requests, and forwards it to the right controller.

Puma is set to run on cluster mode.

  • A Master puma process will execute when we run rails
    • Tn production, we would start Thurster, which is the default http/2 proxy that ships with rails.

puma.rb

on_worker_boot

Analogy and mermaid diagrams generated by Gemini Pro 2.5 Reviewed and edited by Ali

Imagine you're running a restaurant.

  • The Puma Master Process is the Head Chef.
  • The Worker Processes are the Line Cooks that the Head Chef hires and trains.
  • The Threads in each worker are the number of pans each Line Cook can have on the stove at once.
  • The Database & Redis are the Pantry and Fridge.
  • A Network Connection is a Key to the Pantry/Fridge.

The Core Problem: Photocopying the Key

Puma's cluster mode is designed for efficiency. The Head Chef (Master Process) first sets up the kitchen by loading the entire Rails application (preload_app!).

  • This is like preparing all the recipe books and ingredients.

Now, to handle more customers, the Head Chef decides to hire two Line Cooks (Worker Processes).

  • workers(ENV.fetch('WEB_CONCURRENCY') { 1 })
  • Instead of re-training each one from scratch, the Chef uses a magical photocopier (fork()) to create perfect clones of themself.

Here's the critical part: When the Head Chef (Master) opens the connection to the Pantry (Database/Redis), they get a single, specific key.

  • When they photocopy themself to create the Line Cooks (Workers), each cook gets a photocopy of that same key.

Now you have two cooks (Worker 1, Worker 2) thinking they have their own private key, but they're both using a copy of the original.

What happens?

  • Cook 1 goes to the pantry, unlocks it, and starts getting an ingredient.
  • Cook 2, using their copied key, barges in at the same time.
  • They bump into each other, drop ingredients, get the wrong orders, and make a mess. The data they get is garbled and corrupted.

This is exactly what happens with your Redis and ActiveRecord connections.

  • The forked worker processes all end up with a shared, copied network connection (known as a file descriptor or socket), leading to chaos and data corruption.

The Solution: Give Each Cook Their Own Key

The on_worker_boot function is the solution. It's a special instruction from the Head Chef (Puma Master) that says:

"To each new Line Cook I just hired: The key you have is a useless photocopy. Throw it away. Now, go to the manager and get your own, brand new, personal key to the pantry."

This is what Rails.cache.reconnect and ActiveRecord::Base.establish_connection do. They tell each worker process to discard the inherited, copied connection and establish its own fresh, clean, and private connection to Redis and the database.

Visualizing the Process

Here is a diagram illustrating the wrong way vs. the right way.

The WRONG Way: Connecting Before the Fork

This is what happens if you don't have an on_worker_boot hook.

In this scenario, both workers are trying to talk over the same line. It's a mess.


The RIGHT Way: Using on_worker_boot

  1. Rails app loads, Puma's Master Process starts
  2. Puma sees config that they need to start in cluster mode. Master Process then executes the function fork() to make a copy of itself
  3. Puma sees the config on_worker_boot, so it will create new connection pools for External Services (i.e. ActiveRecord and Redis)
  4. Now each worker thread has a new connection pool for the External Services

Here, each worker process establishes its own set of connections after it has been created.

  • Worker 1 has its own private connection pool
    • Worker 2 has its own.

They are completely isolated and can't interfere with each other.

Summary

By doing this, we ensure process isolation and resource management.

1. Stability

By forcing each worker to create its own connections, you eliminate the risk of unpredictable concurrency-related bugs that are a nightmare to debug. Your application becomes stable and reliable under load.

2. Scalability

This model scales horizontally. Whether you run 2 workers or 50 workers, the principle remains the same. Each new process is a self-contained unit with its own resources, preventing bottlenecks and interference.

3. Performance

preload_app! in config/puma.rb gives you a fast startup time for new workers because the Rails code is already loaded (copy-on-write memory). The on_worker_boot step adds a tiny one-time cost to worker startup, but it's a necessary investment for the massive stability gains.