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 Rails | fix |
|---|---|
| class variables or class-level instance variables mutated per request | ActiveSupport::CurrentAttributes for per-request state |
@cache ||= {} at class level, shared across threads | Concurrent::Map or Rails.cache |
| gems that are not thread-safe | check before raising thread counts |
| database pool smaller than threads | ActiveRecord::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.