Three models, one decision

Free preview 4 min to read

A ticket lands on your board: "the checkout page calls four services and takes 1.8 seconds". You already know the fix is to stop calling them one after another. What you have to decide is which of Ruby's concurrency models to reach for, and that decision has three real candidates, not one.

Ruby gives you threads, processes and Ractors. They aren't three versions of the same thing. They differ on two axes that decide everything else. Do they run Ruby code at the same time on different cores? And can they see each other's objects?

Model True parallelism Shares objects Cost to start
Thread No, for Ruby code Yes, all of it Cheap
Process Yes No Expensive
Ractor Yes Only shareable ones Moderate
Fiber No Yes, same thread Very cheap

That table is the course. Everything else is detail about how each row behaves, and which row your workload belongs in.

The one fact you need up front

In CRuby, threads don't run Ruby code in parallel. A global lock lets only one thread interpret Ruby at a time. So four threads doing pure computation finish in about the same wall-clock time as one thread doing all four pieces in a row.

Here's that measured on the machine this course was written on, with Ruby 4.0.1, summing squares up to twenty million twice:

sequential  2.01s
threads     2.01s
ractors     1.32s

Threads bought nothing, and Ractors cut the time by about a third. A rerun on a different machine on 2026-09-28 gave 1.64 s sequential, 1.67 s with threads and 0.82 s with Ractors. The shape held, and there the Ractors halved it. That's the whole shape of the decision in one measurement.

What the lock protects, when it's released and how to measure its effect properly is a big enough subject for its own course: the senior course "Ruby: GVL deep dive", which comes right after this one. Here you need the consequence, not the mechanism. Threads are for waiting, not for computing.

Waiting isn't computing

That distinction does more work than it looks. Most of what a Rails process does is wait: for Postgres to answer, for an HTTP call to come back, for a file to be read. While a thread waits on I/O, it isn't interpreting Ruby, so other threads run. Ten threads, each waiting 200 ms on a different service, finish together in about 200 ms, on one core, in one process.

Swap that HTTP call for a JSON parse of a 40 MB payload, and the same ten threads take about ten times as long as one. It's the same code shape with the opposite result, because one workload waits and the other computes.

So the first question about any workload is never "how do I make this concurrent?" It's "is this thing waiting, or working?"

Where you already are

The junior course "Ruby: threading basics" gave you the mechanics. It covered Thread#status, abort_on_exception, a real two-mutex deadlock, Queue and SizedQueue, and a benchmark showing that I/O-bound threads overlap while CPU-bound threads don't. This course assumes all of it and doesn't re-teach how to start a thread.

What changes at this level is that you stop being handed the model and start choosing it, for a workload with a bill and an on-call rotation attached.

What this course covers

Lesson 2 takes threads and Fibers together, because they're the two models that share memory inside one thread of control. It covers what threads are good at, what a Fiber is, and where a Fiber scheduler sits in Ruby 4.0.1.

Lesson 3 is processes: fork, copy-on-write and isolation. It's the model Puma workers and Solid Queue use in production, with real parallelism paid for in memory.

Lesson 4 is Ractors and their sharing rules. Only shareable objects cross the boundary; everything else is copied. That rule isn't a limitation bolted onto the design. It is the design.

Lesson 5 looks at Ractors honestly. It covers the experimental warning the API still prints, and a real Ractor::IsolationError raised by this application's own code when a model class is touched from a non-main Ractor.

Lesson 6 treats thread safety as something you design, instead of something you hope for. It covers shared mutable state, Mutex, Queue as the hand-off primitive and per-request state, and it carries the coding challenge.

Lesson 7 applies a decision framework to three real workloads. Then it walks into a Rails process to see which models are already running there.

Start with threads. They're the model you'll reach for most often, and the one whose limits are easiest to misread.

Lesson 1 of 7 · free

Keep going with lesson 2

Threads and Fibers: concurrency without parallelism

Shows how threads overlap I/O waits, why they share everything, and where Fibers and fiber schedulers fit.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...