Thread-Unsafe Global State
Class variables (@@var), mutable constants, or misuse of CurrentAttributes to store per-request state, producing race conditions in multi-threaded Puma where concurrent requests read and write shared memory, leaking user A’s data into user B’s response.
Before / After
class AuthContext
@@current_user = nil
def self.user=(user)
@@current_user = user
end
def self.user
@@current_user
end
end
# In ApplicationController
before_action do
AuthContext.user = @current_user
end
# Request A sets user 1.
# Request B sets user 2 a millisecond later.
# Request A reads @@current_user => sees user 2.
# Request A renders user 2's private data. # Rails' CurrentAttributes uses thread-local storage,
# scoped to the request, cleared on completion.
class Current < ActiveSupport::CurrentAttributes
attribute :user
attribute :request_id
resets { Time.zone = nil }
end
# In ApplicationController
before_action do
Current.user = @current_user
Current.request_id = request.request_id
end
# Accessed anywhere
Current.user # => the authenticated user of THIS request
# Cleared between requests. No cross-request leakage.
# For objects passed through long call chains,
# prefer explicit dependency injection:
def call(user:, account:)
Reports::Generator.new(user: user, account: account).call
end Why this hurts
Class variables in Ruby are shared across all instances and inheriting classes within a single process. When Puma runs in threaded mode (the default since Rails 5), multiple request threads execute inside the same process and share class-variable memory. Request A writes @@current_user = User.find(1) and begins processing. Request B, scheduled onto a different thread half a millisecond later, writes @@current_user = User.find(2). Request A’s subsequent read of @@current_user returns User 2, not User 1. Authorization decisions, log lines, rendered templates, and emitted emails now reference the wrong user.
The race condition is timing-dependent and almost never reproduces in tests because RSpec runs single-threaded. The failure surfaces only in production under concurrent load, and the symptom is the worst kind of bug: intermittent wrong-user data leakage that looks like fraud or a security incident rather than a code issue. Users report seeing another user’s dashboard, customer support escalates to security, and the engineering team spends days reproducing a bug that is fundamentally about thread scheduling.
Mutable constants (SETTINGS << {key: value}) have the same failure mode: Ruby’s Global VM Lock serializes bytecode execution but not memory visibility, and a mutation by one thread becomes visible to other threads at unpredictable moments. CurrentAttributes uses Ruby’s Thread.current as the backing store, which is request-scoped when Rails’ middleware correctly resets it between requests, but a job that spawns background threads inside a request inherits the parent thread’s Current values and may outlive the request that populated them.
The correct model passes request-scoped context explicitly through method arguments or constructor injection. The overhead of carrying an extra argument through a call chain is a fixed code-hygiene cost; the upside is that concurrency bugs become impossible to introduce by construction. When CurrentAttributes is used, it must be treated as read-only after initial population in the controller, never mutated mid-request, and never accessed from Sidekiq workers or background threads spawned from the request.
Get Expert Help
Inheriting a legacy Rails codebase with this problem? Request a Technical Debt Audit.