rubyguides

The concurrent-ruby Library: Futures, Actors, and Queues

Intro Context

Ruby’s built-in Thread and Mutex classes take you a long way, but they leave a lot of coordination work in your hands. The concurrent-ruby library fills this gap by providing thread-safe collections, futures, actors, and semaphores that would otherwise require custom glue code. Instead of reimplementing concurrency patterns for every project, you get a higher-level toolbox that handles the coordination details for you.

That makes the library especially useful in applications that mix I/O, background work, and shared state. Instead of stitching together custom wrappers around arrays, hashes, and locks, you can lean on tested abstractions that already model the behavior you want. This guide walks through the pieces you are most likely to reach for in day-to-day Ruby code.

The concurrent-ruby library is also a good fit when you want a clearer boundary between the work you start and the work you wait for later. Futures and actors give you that separation explicitly, which makes it easier to describe concurrent behavior in a way other people can read without learning a pile of custom helper methods first.

This guide covers the parts of concurrent-ruby that come up most often: thread-safe data structures, the promises/futures API, synchronization primitives, and the actor model.

TL;DR

Use concurrent-ruby when you want higher-level tools for thread-safe collections, futures, actors, and coordination primitives. It helps you avoid repeating the same locking patterns across your app and gives you a clearer vocabulary for shared work.

thread-safe collections

Ruby’s built-in Array and Hash are not thread-safe. If two threads push to the same array simultaneously, you can lose writes. The same goes for hash key assignments. You can protect them with a Mutex, but that’s verbose and easy to get wrong.

concurrent-ruby gives you drop-in replacements. The key advantage is that every mutating method on these collection types is already synchronized, so you do not need to wrap every access in a mutex block. This makes the code shorter and eliminates a class of bugs where a developer forgets to acquire the lock before reading or writing. The example below shows ten threads each pushing 100 values into a shared array, producing exactly 1000 entries without any lost writes:

require 'concurrent'

arr = Concurrent::Array.new
threads = 10.times.map do
  Thread.new do
    100.times { arr << Thread.current.object_id }
  end
end
threads.each(&:join)
puts arr.size  # => 1000

Every method on Concurrent::Array is synchronized. You get the same guarantee with Concurrent::Hash and Concurrent::Set. No mutex boilerplate required.

h = Concurrent::Hash.new
threads = 5.times.map do |i|
  Thread.new { h[:key] = i }
end
threads.each(&:join)
puts h[:key]  # Safely written, one of the values

One caveat: on CRuby, Concurrent::Array locks against the object itself. In deeply nested concurrent operations you can still hit contention issues, but for most use cases these structures work correctly.

These containers are a good first step when you want safer shared state without changing the rest of your design. They protect the collection itself, but they do not solve every concurrency issue for you. If the objects inside are mutable, you still need to decide who owns them and when they can be changed.

async computation with promises

The Concurrent::Promises module gives you futures — objects representing values that may not exist yet. This is the most powerful part of the library for async work.

futures

Concurrent::Promises.future runs a block on a thread pool immediately and returns a Future:

require 'concurrent'

future = Concurrent::Promises.future do
  sleep 0.5
  42
end

puts future.completed?  # => false (still running)
result = future.value   # Blocks until done, returns 42
puts future.completed?  # => true

The Future object gives you several ways to interact with it:

MethodBehavior
.valueBlocks and returns the result; raises if the block threw
.value(timeout: 1)Blocks with a timeout; returns nil if it times out
.completed?Non-blocking check
.reasonThe exception if the block failed
.waitBlocks until completed without returning a value

delays

A delay does not run until something asks for its value. The block you pass to delay sits idle until the first call to .value, at which point the computation runs and the result is memoized for all future calls. This is especially useful when you want to defer expensive setup work like loading a large configuration file or initializing a connection pool until the moment the result is actually needed:

delay = Concurrent::Promises.delay { expensive_computation }
# Nothing has run yet
result = delay.value  # Now it runs — and memoizes the result

This is useful when you want to defer expensive work until the result is actually needed.

composing futures

Concurrent::Promises.zip combines multiple futures into one that resolves when all complete:

f1 = Concurrent::Promises.future { sleep 0.3; 1 }
f2 = Concurrent::Promises.future { sleep 0.1; 2 }
f3 = Concurrent::Promises.future { sleep 0.2; 3 }

combined = Concurrent::Promises.zip(f1, f2, f3)
results = combined.value  # => [1, 2, 3]

If you need the first result rather than all results, use any_fulfilled_future. For racing on any settlement (success or failure), use any_resolved_future.

This API is especially helpful when your code needs to start multiple independent tasks and then combine the results. It is a good fit for fan-out requests, background orchestration, or any place where waiting on one slow operation would waste the time available to the rest of the program.

Beyond futures, concurrent-ruby also provides synchronization primitives that control how threads access shared resources. These primitives cover patterns that a single mutex cannot express on its own, such as limiting concurrent access to a fixed number, waiting for a group of threads to finish, or allowing multiple readers while blocking writers.

synchronization primitives

Sometimes you need to coordinate threads beyond what a mutex can do. concurrent-ruby provides several primitives for this.

semaphore

A semaphore manages access to a shared resource with a fixed number of permits. Each acquire blocks when no permits are available, and each release gives one back. This is useful for limiting parallelism, such as throttling API calls:

require 'concurrent'

semaphore = Concurrent::Semaphore.new(3)  # Max 3 concurrent accesses

10.times.map do |i|
  Thread.new do
    semaphore.acquire
    puts "Thread #{i} acquired"
    sleep 1
    puts "Thread #{i} releasing"
    semaphore.release
  end
end.each(&:join)

countdown latch

A latch is a one-time barrier that works in a single direction: once the count reaches zero, all waiting threads are released and the latch stays open permanently. This is the right tool when a set of worker threads must all finish before the main thread proceeds, such as during application startup where several services need to initialize before the server begins accepting requests:

require 'concurrent'

latch = Concurrent::CountDownLatch.new(3)

threads = 3.times.map do |i|
  Thread.new do
    sleep rand
    puts "Thread #{i} calling count_down"
    latch.count_down
  end
end

puts "Main thread waiting..."
latch.wait  # Blocks until all 3 count_down calls
puts "All threads done!"

Once a latch reaches zero it stays at zero — it’s one-way.

cyclic barrier

Unlike a latch, a barrier is reusable. N threads all call wait, and when the Nth thread arrives, all are released simultaneously; the barrier resets:

require 'concurrent'

barrier = Concurrent::CyclicBarrier.new(3)

3.times.map do |i|
  Thread.new do
    puts "Thread #{i} waiting at barrier"
    barrier.wait
    puts "Thread #{i} passed!"
  end
end.each(&:join)
# All three print "waiting" then all three print "passed!" together

This is useful when you need threads to rendezvous at a checkpoint before proceeding.

read-write lock

A read-write lock allows multiple concurrent readers but a single exclusive writer:

require 'concurrent'

rwlock = Concurrent::ReadWriteLock.new
shared_data = 0

readers = 5.times.map do
  Thread.new { rwlock.with_read_lock { shared_data } }
end

writer = Thread.new do
  rwlock.with_write_lock { shared_data = 42 }
end

readers.each(&:join)
writer.join

The with_read_lock and with_write_lock helpers acquire and release the lock automatically.

actor model

concurrent-ruby includes an Erlang-style actor implementation. Each actor runs on its own thread, processes messages from its mailbox sequentially, and maintains its own internal state. Because only one message is processed at a time, there are no race conditions inside the actor:

require 'concurrent/actor'

counter = Concurrent::Actor.define(:Counter) do |initial|
  count = initial
  loop do
    message = receive
    case message
    when :inc then count += 1
    when :dec then count -= 1
    when :get then count
    end
  end
end

counter << :inc
counter << :inc
counter << :get  # => 2

The << operator sends a message to the actor’s mailbox. Actors are especially useful for modeling stateful services where you want thread safety without mutex management.

Supervised actors (restarting on failure) live in the edge channel of the gem, which has a less stable API than the core library.

scheduled execution

Concurrent::ScheduledTask runs a block after a given delay and returns a Future:

require 'concurrent'

task = Concurrent::ScheduledTask.execute(2) { 42 }
puts task.completed?  # => false
sleep 3
puts task.completed?  # => true
puts task.value       # => 42

This is the foundation of delayed and periodic job scheduling, and it’s what Sidekiq uses under the hood for retry logic. Because the returned object is a standard future, you can compose scheduled tasks with other promises, chain callbacks on the result, or check completion status the same way you would with any other future in the library.

error handling

When a future’s block raises an exception, the exception is captured and stored in the future’s .reason:

future = Concurrent::Promises.future { raise "oops" }
future.wait
puts future.reason  # => #<RuntimeError: oops>

Always check .reason or wrap .value in a begin/rescue if your futures might fail. There’s no silent failure — the error is there, but you have to look for it.

how concurrent-ruby compares to alternatives

stdlib Thread gives you raw OS threads. You’re responsible for protecting shared state, avoiding deadlocks, and managing thread lifecycle. concurrent-ruby handles all of that for you through higher-level abstractions.

The async gem uses fibers instead of threads. Fibers are lightweight green threads that cooperate by yielding, so they do not run in parallel on multiple CPU cores, but they use far less memory. If you have thousands of concurrent I/O-bound connections, async wins. If you need true parallelism for CPU-bound work, concurrent-ruby wins.

Once the library feels familiar, the next step is to compare it with the lower-level concurrency tools that Ruby already gives you. The Ruby Threads Basics tutorial shows the shared-memory primitives, while the Ruby Ractors introduction shows the isolated-worker model. That comparison helps you choose the smallest concurrency tool that still fits your workload.

concurrent-ruby runs on CRuby, JRuby, and TruffleRuby. The thread pool executors auto-size based on the runtime environment, so your code doesn’t need to change between implementations.

If you only remember one thing, remember that concurrent-ruby is about coordination first and speed second. The library helps you model shared work safely, but it still works best when the surrounding code makes ownership and responsibility obvious.

summary

concurrent-ruby sits between low-level Ruby threads and a custom concurrency framework. It gives you reliable building blocks for shared collections, task orchestration, and inter-thread communication without making you reinvent those pieces yourself. If your app needs thread-safe coordination, it is one of the most practical gems in the Ruby ecosystem.

See Also