<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Huzaifa Saifuddin</title>
    <description>The latest articles on DEV Community by Huzaifa Saifuddin (@huzaifa_saifuddin_370679c).</description>
    <link>https://dev.to/huzaifa_saifuddin_370679c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195845%2F988acafd-23bf-4e0a-b74e-428da83ca0c3.jpg</url>
      <title>DEV Community: Huzaifa Saifuddin</title>
      <link>https://dev.to/huzaifa_saifuddin_370679c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/huzaifa_saifuddin_370679c"/>
    <language>en</language>
    <item>
      <title>Thread vs Fiber vs Ractor: I Measured Ruby's Concurrency Model</title>
      <dc:creator>Huzaifa Saifuddin</dc:creator>
      <pubDate>Tue, 20 Oct 2026 10:05:32 +0000</pubDate>
      <link>https://dev.to/huzaifa_saifuddin_370679c/thread-vs-fiber-vs-ractor-i-measured-rubys-concurrency-model-2577</link>
      <guid>https://dev.to/huzaifa_saifuddin_370679c/thread-vs-fiber-vs-ractor-i-measured-rubys-concurrency-model-2577</guid>
      <description>&lt;p&gt;A few weeks back, at RailsConf Pune (Deccan Queen), Shopify engineers gave a talk titled &lt;em&gt;"When Rails Met Fibers: Migrating Sidekiq from Puma to Falcon."&lt;/em&gt; Their challenge: an AI chatbot where every request was itself waiting on an external AI API call, and Puma's thread-per-request model was buckling under all that waiting.&lt;/p&gt;

&lt;p&gt;I've used &lt;code&gt;Thread&lt;/code&gt; for over a decade — Puma's thread pool, Sidekiq workers, the usual. But that talk made me want to go past the surface-level API and actually verify what's happening underneath: what the GVL really guarantees, where Fiber genuinely earns its place, and where Ractor fits in. So I sat down and did exactly that — measuring Thread, Fiber, and Ractor myself instead of taking any of it on faith.&lt;/p&gt;

&lt;p&gt;Every number in this post came from a script I ran. The full, runnable repo — one commit per finding — is here: &lt;strong&gt;&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor" rel="noopener noreferrer"&gt;thread-fiber-ractor on GitHub&lt;/a&gt;&lt;/strong&gt;. Clone it and run the scripts yourself; your numbers will differ from mine (different machine, different core count), and that's kind of the point — don't trust my numbers either, verify them.&lt;/p&gt;

&lt;p&gt;Built and run on Ruby 4.0.7.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8wv9m2w17ghiyyocqzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg8wv9m2w17ghiyyocqzi.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The GVL, the one idea everything else builds on
&lt;/h2&gt;

&lt;p&gt;MRI's Global VM Lock (GVL) lets exactly one thread execute a Ruby instruction at any given instant, in one process — no matter how many threads you spawn. That's the single fact that explains almost everything below.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A thread blocked on I/O (a network call, a DB round-trip) &lt;strong&gt;releases the GVL&lt;/strong&gt;, so another thread can run while it waits.&lt;/li&gt;
&lt;li&gt;A thread doing pure CPU work &lt;strong&gt;won't release it voluntarily&lt;/strong&gt; — but it's not quite "holds the lock forever" either. MRI's VM has an internal timer that can still force a switch between CPU-bound threads. The catch: that forced switch just means CPU-bound threads take turns. It doesn't make them any faster, because only one of them is ever actually executing at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second point is the one worth sitting with, because it's exactly what separates Thread from Fiber later in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thread: concurrency on I/O, nothing on CPU
&lt;/h2&gt;

&lt;p&gt;Here's the proof, straight from &lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/01_gvl_thread_basics.rb" rel="noopener noreferrer"&gt;&lt;code&gt;01_gvl_thread_basics.rb&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"benchmark"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_primes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;upto&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;is_prime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="no"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;none?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_prime&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;count&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;

&lt;span class="c1"&gt;# CPU-bound: does Thread give you parallelism?&lt;/span&gt;
&lt;span class="n"&gt;serial&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;realtime&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;count_primes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;N&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;count_primes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;parallel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;realtime&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;count_primes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;count_primes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# I/O-bound: does Thread give you concurrency?&lt;/span&gt;
&lt;span class="n"&gt;serial_io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;realtime&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;parallel_io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Benchmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;realtime&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- CPU-bound: does Thread give you parallelism? ----
Two calls, one after another:   5.77s
Same two calls, on two Threads:  5.79s
speedup: 1.0x  (expect ~1.0x — the GVL serializes CPU-bound Ruby)

---- I/O-bound: does Thread give you concurrency? ----
Two sleeps, one after another:  2.0s
Same two sleeps, on two Threads: 1.01s
speedup: 1.98x  (expect ~2.0x — I/O releases the GVL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical CPU-bound work: &lt;strong&gt;zero speedup&lt;/strong&gt; across two threads — in fact marginally slower (5.79s vs 5.77s), which lines up with forced-switch overhead rather than one thread holding the lock uninterrupted. The same two threads doing I/O get a real ~2x speedup, because I/O actually releases the lock.&lt;/p&gt;

&lt;p&gt;This is Thread's whole story: great concurrency primitive for I/O you already have (that's what Puma's thread pool and Sidekiq workers are doing for you), zero help for CPU-bound work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hidden ceiling
&lt;/h3&gt;

&lt;p&gt;Threads map to real OS kernel threads, and the kernel hard-caps how many of those can exist. On my machine (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/02_thread_ceiling.rb" rel="noopener noreferrer"&gt;&lt;code&gt;02_thread_ceiling.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Creating Threads until the OS refuses...
Hit a hard OS wall at 2046 threads -- can't create Thread: Resource temporarily unavailable

Creating 50000 Fibers (not an OS construct — no equivalent ceiling)...
Created all 50000 Fibers with zero errors (24.4x past where Threads hit their wall)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2,046 threads and the OS refuses to create more. 50,000 Fibers, in the same process, zero errors — because a Fiber isn't an OS-level construct at all. Which is a good place to introduce Fiber properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fiber: cooperative, cheap, and not a speed trick
&lt;/h2&gt;

&lt;p&gt;A Fiber is a coroutine: pause and resume control flow, entirely inside one Ruby process, with no OS involvement. &lt;code&gt;Fiber.new&lt;/code&gt; creates one, &lt;code&gt;Fiber.yield&lt;/code&gt; pauses it, &lt;code&gt;#resume&lt;/code&gt; continues it (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/03_fiber_basics.rb" rel="noopener noreferrer"&gt;&lt;code&gt;03_fiber_basics.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;fiber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Fiber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"fiber: step 1"&lt;/span&gt;
  &lt;span class="no"&gt;Fiber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"fiber: step 2"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "fiber: step 1"&lt;/span&gt;
&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "fiber: step 2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail that trips people up: a Fiber only ever runs on whichever OS thread calls &lt;code&gt;#resume&lt;/code&gt;. Resuming it from a different thread raises &lt;code&gt;FiberError: fiber called across threads&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fiber alone is not fast
&lt;/h3&gt;

&lt;p&gt;It's tempting to assume Fiber is a speed trick because it's a newer, fancier control-flow tool. It isn't. A Fiber switch has real overhead, and for a simple "stop when you hit a condition" loop, a plain loop wins every time (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/04_fiber_is_not_a_speed_trick.rb" rel="noopener noreferrer"&gt;&lt;code&gt;04_fiber_is_not_a_speed_trick.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plain loop: 0.0824s
fiber:      0.5839s
fiber is 7.1x SLOWER than a plain loop for this
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fiber's value isn't raw speed. It's structural — which is exactly what Fiber::SchedulerInterface unlocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real payoff: Fiber::SchedulerInterface
&lt;/h3&gt;

&lt;p&gt;Registering a scheduler (via &lt;code&gt;Fiber.set_scheduler&lt;/code&gt;) makes Ruby intercept blocking calls — &lt;code&gt;sleep&lt;/code&gt;, socket I/O — and hand them to the scheduler instead of actually blocking the OS thread. The scheduler registers interest with the kernel's own I/O multiplexer (epoll/kqueue), and the thread is immediately free to run other ready fibers. &lt;strong&gt;This is the exact mechanism behind Falcon&lt;/strong&gt; — the app server from the RailsConf talk that kicked this whole thing off.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;async&lt;/code&gt; gem is the reference implementation. Here's the proof (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/05_fiber_scheduler.rb" rel="noopener noreferrer"&gt;&lt;code&gt;05_fiber_scheduler.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"async"&lt;/span&gt;

&lt;span class="no"&gt;Async&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="mi"&gt;3_000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- Threads: 20 concurrent 'slow I/O calls' (sleep 1 each) ----
  wall time: 1.01s
  OS threads used: 20 (one per call)

---- Fibers + scheduler: same 20 calls, one task.async each ----
  wall time: 1.0s
  OS threads used: 1 (every task cooperatively shared the same thread)

---- Now push past where Threads hit their wall (~2,046) ----
Running 3000 concurrent 'slow calls' as Fibers -- Thread.new would have crashed by now.
  wall time: 1.11s
  3000 concurrent I/O-bound calls, 1 OS thread, no crash.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3,000 concurrent "I/O calls," one OS thread, 1.11 seconds. That's something Thread literally cannot attempt past its own ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  The cost: no preemption safety net
&lt;/h3&gt;

&lt;p&gt;Here's the part that doesn't make it into the pitch. Thread's GVL still has that forced timer switch we talked about earlier — even a pure CPU loop gets preempted eventually. Fiber has nothing like it. A fiber only ever gives up the thread when &lt;strong&gt;it&lt;/strong&gt; decides to, by hitting a blocking call. A CPU-bound fiber with zero yield points starves every other fiber on that thread for its entire run — even ones whose I/O already finished (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/06_fiber_has_no_preemption.rb" rel="noopener noreferrer"&gt;&lt;code&gt;06_fiber_has_no_preemption.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Async&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;busy_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;        &lt;span class="c1"&gt;# pure CPU, no yield point at all&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;          &lt;span class="c1"&gt;# "ready" almost immediately&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CPU-bound fiber] finished after 2.0s
[I/O fiber]        finished after 2.1s (expected ~0.1s if it weren't starved)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The I/O fiber was ready in 0.1 seconds. It didn't actually finish until 2.1 seconds — starved the entire time by a CPU-bound fiber that never yielded. If that CPU-bound fiber ran forever, so would the wait. There's no timeout coming to rescue you.&lt;/p&gt;

&lt;p&gt;That tradeoff — massive I/O concurrency, zero CPU-starvation protection — is almost certainly the exact thing Shopify had to reason about migrating to Falcon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ractor: actual parallelism
&lt;/h2&gt;

&lt;p&gt;Thread gives you concurrency, never parallelism, on CPU-bound work — that's the GVL's whole deal. Ractor is the one primitive that breaks that rule entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ractor basics
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/07_ractor_basics.rb" rel="noopener noreferrer"&gt;07_ractor_basics.rb&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"hello from a ractor"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "hello from a ractor"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noting: Ractor's API is still marked experimental and &lt;strong&gt;has changed across Ruby versions&lt;/strong&gt; — an older &lt;code&gt;#take&lt;/code&gt; method from some tutorials doesn't exist anymore; &lt;code&gt;#value&lt;/code&gt; is the current way to get a Ractor's result. Don't trust a blog post's (including this one's) method names without checking against your own Ruby version.&lt;/p&gt;

&lt;h3&gt;
  
  
  The sharing rules
&lt;/h3&gt;

&lt;p&gt;Since Ractors genuinely run at the same instant, Ruby has to prevent two of them from mutating the same object at once. It does this by making the setup physically impossible, not just "risky" (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/08_ractor_sharing_rules.rb" rel="noopener noreferrer"&gt;&lt;code&gt;08_ractor_sharing_rules.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# closures over a mutable outer variable are refused immediately&lt;/span&gt;
&lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; ArgumentError: can not isolate a Proc because it accesses outer variables (arr)&lt;/span&gt;

&lt;span class="c1"&gt;# #send of a mutable object copies it&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; false, it's a copy&lt;/span&gt;

&lt;span class="c1"&gt;# a frozen object is shared by TRUE reference&lt;/span&gt;
&lt;span class="n"&gt;frozen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object_id&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; true, zero-copy&lt;/span&gt;

&lt;span class="c1"&gt;# Ractor.make_shareable deep-freezes recursively&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;roles: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lead"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"engineer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make_shareable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frozen?&lt;/span&gt;          &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:roles&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;frozen?&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; true, the nested array too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put together: there is no path to "two Ractors holding a live reference to the same mutable object." Not a risky one, not a rare one — none. If you need real shared &lt;em&gt;mutable&lt;/em&gt; state across Ractors, the only sanctioned path is message-passing — each Ractor owns its own private state, and they coordinate by sending copies (or transferring ownership via &lt;code&gt;move: true&lt;/code&gt;) across, never by two of them reaching into the same box at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  The payoff: real CPU speedup
&lt;/h3&gt;

&lt;p&gt;Same CPU benchmark as the Thread section, now split across Ractors instead (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/09_ractor_vs_thread_cpu.rb" rel="noopener noreferrer"&gt;&lt;code&gt;09_ractor_vs_thread_cpu.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;ractors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;Ractor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;count_primes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;ractors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- Serial: 4 calls, one after another ----
  wall time: 5.79s

---- Threads: 4 concurrent, same GVL ----
  wall time: 5.82s
  speedup vs serial: 0.99x

---- Ractors: 4 concurrent, independent GVLs ----
  wall time: 1.73s
  speedup vs serial: 3.35x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same workload, same machine, same core count available the whole time. Thread: flat. Ractor: 3.35x faster. The only variable that changed is whether each worker shares one GVL or gets its own.&lt;/p&gt;

&lt;p&gt;Worth knowing before you reach for this in a Rails app: &lt;strong&gt;ActiveRecord isn't Ractor-safe yet.&lt;/strong&gt; I tried calling &lt;code&gt;Rails.application&lt;/code&gt; and &lt;code&gt;ApplicationRecord.connection&lt;/code&gt; from inside a Ractor and both hard-failed — &lt;code&gt;Rails.application&lt;/code&gt; raises &lt;code&gt;Ractor::IsolationError&lt;/code&gt; because &lt;code&gt;Rails&lt;/code&gt; itself holds the booted app in a mutable instance variable only the main Ractor can read. So the practical pattern is: pull data out of Rails/AR &lt;em&gt;first&lt;/em&gt;, on the main Ractor, then hand off only plain, frozen data to a pool of Ractors for the CPU-heavy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: the Rails &lt;code&gt;Current.user&lt;/code&gt; gotcha under Fiber concurrency
&lt;/h2&gt;

&lt;p&gt;This one connects straight back to the Falcon migration. Most Rails apps keep request-scoped context — current user, request id — via &lt;code&gt;ActiveSupport::CurrentAttributes&lt;/code&gt;. It's built on &lt;code&gt;ActiveSupport::IsolatedExecutionState&lt;/code&gt;, which &lt;strong&gt;defaults to &lt;code&gt;isolation_level = :thread&lt;/code&gt;&lt;/strong&gt; — storing state as a plain attribute on the shared &lt;code&gt;Thread&lt;/code&gt; object, not scoped per-fiber.&lt;/p&gt;

&lt;p&gt;Under a Fiber-scheduler server like Falcon, multiple fibers share one OS thread. With the default &lt;code&gt;:thread&lt;/code&gt; isolation, they'll clobber each other's &lt;code&gt;Current&lt;/code&gt; state (&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor/blob/main/10_thread_current_gotcha.rb" rel="noopener noreferrer"&gt;&lt;code&gt;10_thread_current_gotcha.rb&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Current&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CurrentAttributes&lt;/span&gt;
  &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:request_id&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Async&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"req-A"&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request_id&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; ???&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"req-B"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- isolation_level = :thread (Rails' default) ----
  [A] woke back up. Current.request_id is now: "req-B"

---- isolation_level = :fiber ----
  [A] woke back up. Current.request_id is now: "req-A"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the default, fiber A's own request id got overwritten by fiber B's — a different request's data leaking into fiber A's execution. Rails actually ships a real fix for this: &lt;code&gt;config.active_support.isolation_level = :fiber&lt;/code&gt;. If your app ever moves to a Fiber-scheduler server, this is a config line worth knowing exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one do you actually need?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thread&lt;/strong&gt; — concurrency on I/O you already have (Puma, Sidekiq). Never parallelism on CPU-bound Ruby. Hard OS ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fiber&lt;/strong&gt; — I/O concurrency at a scale Thread can't reach (thousands of fibers, one OS thread). Cooperative, with zero preemption safety net.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ractor&lt;/strong&gt; — compute parallelism Thread can never give you. The price is real isolation: no shared mutable state, enforced by the VM, and (for now) no ActiveRecord inside one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these replace the others. Pick based on where your actual bottleneck is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;All ten scripts, with real output and one commit per finding: &lt;strong&gt;&lt;a href="https://github.com/HuzaifaSaifuddin/thread-fiber-ractor" rel="noopener noreferrer"&gt;github.com/HuzaifaSaifuddin/thread-fiber-ractor&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/HuzaifaSaifuddin/thread-fiber-ractor
&lt;span class="nb"&gt;cd &lt;/span&gt;thread-fiber-ractor
bundle &lt;span class="nb"&gt;install
&lt;/span&gt;ruby 01_gvl_thread_basics.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your numbers will come out different from mine — different machine, different core count, different load. Run them anyway. That's the whole point of this post: stop trusting the docs, go measure it.&lt;/p&gt;

&lt;p&gt;If you've used Fiber or Ractor in a real app — or migrated a service to Falcon — I'd genuinely like to hear what you ran into. Drop it in the comments.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>performance</category>
      <category>ruby</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
