One criticism of Ruby still seems to be the idea of it "not scaling," while JavaScript is praised for its async superpowers. But here's the thing: Ruby can do async just fine. We just don't use it as much.
Let's be real about what Ruby can and can't do—and why the perception gap exists.
The Problem: Sequential Slowness
Here's typical Ruby code:
def call
create_record_one # 2 seconds
create_record_two # 2 seconds
create_record_three # 2 seconds
end
# Total: 6 secondsEach operation waits for the previous one to finish. If these are I/O-bound tasks (database writes, API calls), we're wasting time.
The Solution: Concurrent Execution
require 'concurrent-ruby'
def call
promises = [
Concurrent::Promise.execute { create_record_one },
Concurrent::Promise.execute { create_record_two },
Concurrent::Promise.execute { create_record_three }
]
promises.each(&:value!) # Wait for all, raise any errors
end
# Total: ~2 secondsSame work. One-third the time.
Modern Ruby Has Even Better Options
Ruby 3.x introduced Fibers and the async gem gives you a more natural async/await style:
require 'async'
def call
Async do
Async do
create_record_one
end
Async do
create_record_two
end
Async do
create_record_three
end
end
endTo me this looks closer to JavaScript's event loop model.
The Honest Truth: Where Ruby Falls Short
Let's not pretend there aren't trade-offs:
1. The GIL Problem MRI Ruby has a Global Interpreter Lock. For CPU-bound work (heavy computation, image processing), you won't get true parallelism across cores. Multiple threads can't execute Ruby code simultaneously. JavaScript doesn't have this limitation for async I/O (though it's single-threaded for CPU work too).
2. Async Isn't the Default JavaScript's async is baked into the language from day one as the event loop is fundamental. Ruby bolted concurrency on later. This means:
- Less community momentum around async patterns
- Fewer libraries designed async-first
- More friction integrating async into existing codebases
3. Complexity Tax Async code is harder to reason about. Debugging race conditions, managing shared state, handling errors across threads is more complex, especially if you do not do it often. Sequential code is simpler, and for many apps, the performance hit doesn't matter.
So When Does Ruby Async Make Sense?
Use it when:
- You have I/O-bound operations (API calls, database queries, file operations)
- Operations can run independently
- You're hitting performance bottlenecks in sequential code
Skip it when:
- Operations are fast already
- They depend on each other's results
- The added complexity isn't worth the performance gain
- You're doing heavy CPU work (the GIL will limit you anyway)
Why Don't We Do This More?
- Rails doesn't encourage it - The thread-per-request model works fine for most apps. Why add complexity?
- JavaScript made async unavoidable - Node forced developers to think async. Ruby lets you choose.
- The "Ruby doesn't scale" meme - It's easier to say "Ruby is slow" than to actually measure and optimize
The Real Takeaway
For I/O-bound work (which is most web apps), async Ruby is just as capable as async JavaScript. The tools exist: concurrent-ruby, async, Fibers, even good old threads.
JavaScript makes async the default. Ruby makes it opt-in. That doesn't mean Ruby can't do it but as a tool we're just not opting in enough.
Not every app needs to be async-first. But when you need it, Ruby's got you covered.