When you are using Unicorn or Phusion Passenger (Community Edition), you typically don’t have to worry about thread safety because these servers are multi-process (worker-based).

Read more about application servers here.

However, Puma comes with multi-threaded options. In the MRI ecosystem, we often don’t prioritize writing thread-safe code, which can cause issues in a multi-threaded environment like Puma. If you are migrating your application server from Unicorn or Passenger to Puma (with multi-threaded mode enabled), make sure you follow these tips.

1. Good Practice: Use Constants and Freeze Them!

Ruby does not provide immutable data structures by default; even constants are mutable in Ruby’s world.

2.5.0 :001 > IAM_CONSTANT = "test"
 => "test"
2.5.0 :002 > IAM_CONSTANT = "new_test"
(irb):2: warning: already initialized constant IAM_CONSTANT
(irb):1: warning: previous definition of IAM_CONSTANT was here
 => "new_test"
2.5.0 :003 > IAM_CONSTANT
 => "new_test"

2.5.0 :001 > IAM_ARRAY_CONSTANT = ["Haha! I am constant, no one can change me!"]
 => ["Haha! I am constant, no one can change me!"]
2.5.0 :002 > IAM_ARRAY_CONSTANT << "Seriously?"
 => ["Haha! I am constant, no one can change me!", "Seriously?"]
2.5.0 :003 >

To prevent mutation, use .freeze:

2.5.0 :001 > IAM_ARRAY_CONSTANT = ["Haha! I am constant, no one can change me!"].freeze
 => ["Haha! I am constant, no one can change me!"]
2.5.0 :002 > IAM_ARRAY_CONSTANT << "Seriously?"
Traceback (most recent call last):
        2: from /Users/satyanarayan/.rvm/rubies/ruby-2.5.0/bin/irb:11:in `<main>'
        1: from (irb):2
FrozenError (can't modify frozen Array)

2. Don’t Mutate Global Variables

I have worked with legacy codebases where the current user was determined using a global variable. While this might work in a multi-process server like Passenger, it will fail in a multi-threaded environment like Puma.

class UsersController < ApplicationController
  before_action :authenticate_user

  def authenticate_user
    $user_id = fetch_user_from_cookies
    sleep(10)
    if $user_id == "admin"
      Rails.logger.info("Show admin template")
    else
      Rails.logger.info("Show normal template")
    end
  end

  def index
    render json: ["Hey, I am working fine"]
  end
end

In a multi-threaded environment, $user_id is shared across all threads, leading to race conditions where one user’s ID might overwrite another’s.

3. Class Variables and Class Instance Variables are Not Thread-Safe

If you are mutating class variables or class instance variables within a class method, your code is likely not thread-safe and could lead to unpredictable behavior across requests.

4. Memoization: Use it Wisely

Memoization is a great practice for caching values across multiple calls within an object. However, be cautious when memoizing inside class methods, as those values will be shared across all requests in a multi-threaded environment.

class UsersController < ApplicationController
  before_action :fetch_user

  def fetch_user
    @user = User.find_user_once(@user_id)
  end

  def index
    render json: ["Hey, it works"]
  end
end


class User
  def self.find_user_once(user_id)
    @test ||= User.find(user_id) # This is shared across all threads!
  end
end

5. Audit Your Gems

Before adding a new gem to your project, read the documentation or source code to confirm that the library is thread-safe.