The GIL (Global Interpreter Lock) is a mutex inside MRI Ruby that ensures only one thread executes Ruby bytecode at a time. It does not mean Ruby is single threaded, and it does not block all parallel work. It is a design choice that simplifies memory management and keeps C extensions safe.
In MRI Ruby, GIL allows only one thread to execute Ruby bytecode at a time, even on multi-core CPUs.
What the GIL does
- protects MRI internal data structures from concurrent mutation
- allows Ruby threads to exist, but only one runs Ruby code at a time
- releases the lock around many blocking I/O operations
This means CPU-bound Ruby code does not parallelize across cores, while I/O-bound work can still benefit from threads.
Why MRI keeps a GIL
MRI is a C-based interpreter with a large ecosystem of C extensions. The GIL:
- makes object access safer without heavy locking everywhere
- keeps extension authors from handling complex thread safety
- simplifies garbage collection
Other Ruby implementations (JRuby, TruffleRuby) do not use a GIL, but they also have different runtimes and extension models.
Example: I/O-bound work still gains concurrency
In this example, the threads spend most time waiting on I/O, so the GIL is released and requests overlap.
require "net/http"
require "benchmark"
urls = [
"https://example.com",
"https://example.org",
"https://example.net"
]
elapsed = Benchmark.realtime do
threads = urls.map do |url|
Thread.new do
Net::HTTP.get(URI(url))
end
end
threads.each(&:join)
end
puts "Elapsed: #{elapsed.round(2)}s"
You should see elapsed time closer to the slowest request, not the sum of all three. That is real concurrency, even with the GIL.
Example: CPU-bound code does not speed up
This example is CPU-heavy Ruby code, so the GIL prevents true parallelism.
def cpu_work(n)
x = 0
n.times { x += 1 }
x
end
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
threads = 4.times.map do
Thread.new { cpu_work(30_000_000) }
end
threads.each(&:join)
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "Elapsed: #{(finish - start).round(2)}s"
Running this with more threads usually does not speed up on MRI. Use processes or a different Ruby runtime for CPU-bound parallelism.
Common myths about the GIL
- Myth: “Ruby cannot do concurrency.” Reality: Ruby threads are real and great for I/O-bound work.
- Myth: “The GIL makes threads useless.” Reality: Threads still help with waiting on network, disk, or DB.
- Myth: “The GIL is only a bug.” Reality: It is a trade-off that keeps MRI simpler and safer.
- Myth: “The GIL blocks all C extensions.” Reality: Well-written extensions release the GIL for long work.
Practical guidance for Rails
- Use threads for I/O-heavy tasks (HTTP calls, DB reads).
- Use processes for CPU-bound work (image processing, heavy JSON).
- Background jobs can mix both: threads within a process for I/O, multiple worker processes for CPU.
Summary
The MRI GIL is a global lock that serializes Ruby bytecode execution. It limits CPU-bound parallelism but allows effective I/O concurrency. When you know the trade-offs, you can choose the right concurrency model for your Rails workload.
Comments