Ruby Can Be Async Too (You're Just Not Using It)

10 min read

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 seconds

Each 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 seconds

Same 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
end

To 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?

  1. Rails doesn't encourage it - The thread-per-request model works fine for most apps. Why add complexity?
  2. JavaScript made async unavoidable - Node forced developers to think async. Ruby lets you choose.
  3. 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.