GIL: the lock every Rubyist needs to understand
Ruby has no real parallelism because of the GIL — a half-truth said with confidence. Understand the GVL for real, when threads help and when they become traps.
GIL: the lock every Rubyist needs to understand
There's a sentence repeated in dev conversations as absolute truth:
"Ruby has no real parallelism because of the GIL."
Half-truth.
And a half-truth said with confidence is worse than a full lie.
First: GIL or GVL?
In modern MRI, the correct name is GVL (Global VM Lock).
GIL is a term inherited from Python.
It works the same in practice: only one thread executes Ruby code at a time within a single process.
But the "Ruby code" part is important.
Because IO doesn't count.
What the GVL actually does
The GVL serializes YARV bytecode execution.
When a thread is running Ruby code, it holds the lock.
When it goes to wait on:
- file reads
- HTTP connections
- a Postgres query
sleep
…it releases the GVL.
Another thread takes over and runs Ruby code in that interval.
That's why threads in Ruby are still worth it. You just need to understand what for.
When threads help
threads = urls.map do |url|
Thread.new { Net::HTTP.get(URI(url)) }
end
threads.each(&:join)
100 parallel HTTP requests?
Works.
Each thread sits blocked waiting on the network. The GVL gets released. Another thread takes over.
Throughput goes WAY up.
When threads DON'T help
threads = (1..4).map do
Thread.new do
1_000_000.times { Math.sqrt(rand) }
end
end
threads.each(&:join)
Four threads doing pure computation?
Speeds up nothing.
Because it's CPU-bound. All of them compete for the same GVL.
You add context-switch overhead and gain zero parallelism.
Worse: it can get slower.
The right mental image
Ruby Process
├── GVL [🔒]
├── Thread 1 — holds the lock → runs code
├── Thread 2 — waiting on lock
├── Thread 3 — waiting on lock
└── Thread 4 — doing IO (released the lock)
GVL = a single microphone in a group of 4 people trying to talk.
Only whoever has the microphone speaks.
When someone goes to drink water (IO), they pass the microphone.
How to live with this
There are three paths:
1. Multiple processes
Puma worker 1 — own GVL
Puma worker 2 — own GVL
Puma worker 3 — own GVL
Modern Ruby web apps use this model. Each worker has its own GVL. Real parallelism between them.
Cost: more memory.
2. Threads for IO-bound work
Sidekiq processes jobs in threads.
Works because jobs generally run queries, hit APIs, write to disk.
IO all the way down.
3. Get out of MRI
- JRuby: runs on the JVM, no GVL. Real threads.
- TruffleRuby: runs on GraalVM, no GVL.
- Ractor: experiment inside MRI itself, working around the GVL with isolation.
Each has trade-offs. None is free.
Why does the GVL still exist?
Because MRI has C dependencies that assume single-thread.
Removing the GVL would break gems across the whole ecosystem.
The Ruby core team has been working on alternatives (Ractor, fibers). But the transition is gradual.
And it needs to be. A broken migration destroys trust in the ecosystem.
The most common reasoning mistake
"I'll spin up 50 threads to process this list faster."
If it's computation: you just added overhead with no gain.
If it's IO: it might work — but watch out for the connection pool, memory pressure, GC pressure.
Threads are powerful. Threads are also a trap.
The difference is understanding what's blocking.
The big shift
The GVL doesn't stop you from doing concurrency.
The GVL stops you from doing CPU parallelism within a single process.
Those are different things.
Concurrency = dealing with many things that wait.
Parallelism = executing many things at the same time.
Web apps are concurrency.
Heavy numeric processing is parallelism.
Use the right tool for each.
Conclusion
The GVL isn't a defect.
It's a trade-off.
It simplifies the runtime, keeps compatibility with C extensions, and still allows IO concurrency.
If you're suffering with it, you might be using Ruby for something it wasn't built for.
Or you might be using threads the wrong way.
Whoever understands the GVL stops fighting it.
And starts designing systems around it.
