Part 3 · 1 chapters · ~8 min

Concurrency

Threads with the GVL, Puma's workers and threads, thread safety in Rails code (class variables, memoisation), connection pools sized to threads, fibers and the fiber scheduler, async gem, Ractors, and background jobs as the main concurrency tool.

4

Threads, processes and pools

code
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY", 2)          # processes: CPU parallelism
threads 3, 3                                     # threads per process: IO concurrency (Rails default is 3)
preload_app!                                     # load the app once, fork workers (copy-on-write memory)

# config/database.yml: the pool must be at least the thread count (plus job threads in the same process)
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 3) %>
thread-safety hazard in Railsfix
class variables or class-level instance variables mutated per requestActiveSupport::CurrentAttributes for per-request state
@cache ||= {} at class level, shared across threadsConcurrent::Map or Rails.cache
gems that are not thread-safecheck before raising thread counts
database pool smaller than threadsActiveRecord::ConnectionTimeoutError under load: size the pool to threads

Fibers: Ruby 3's fiber scheduler lets IO inside fibers yield automatically; the async gem and Falcon server use it for very high concurrency. For most Rails apps, the concurrency tool that matters most is still Active Job with Sidekiq or Solid Queue: move slow work out of requests.