Leveraging Thread.new for concurrency
Though Matz has expressed regrets about introducing Thread in Ruby, I personally never found it all that intimidating. In fact, I've found the block based API both intuitive and quite beautiful.
threads = 5.times.map do |i|
Thread.new do
start = Time.now
sleep 1
"Thread #{i} ran between #{start} and #{Time.now}"
end
end
return_values = threads.map(&:value)
puts return_values.join("\n")
# Output:
# Thread 0 ran between 2026-04-24 12:00:00 and 2026-04-24 12:00:01
# Thread 1 ran between 2026-04-24 12:00:00 and 2026-04-24 12:00:01
# Thread 2 ran between 2026-04-24 12:00:00 and 2026-04-24 12:00:01
# Thread 3 ran between 2026-04-24 12:00:00 and 2026-04-24 12:00:01
# Thread 4 ran between 2026-04-24 12:00:00 and 2026-04-24 12:00:01
const threads = Array.from({ length: 5 }, (_, i) =>
new Promise((resolve) => {
const start = new Date().toISOString();
setTimeout(() => {
const end = new Date().toISOString();
resolve(`Thread ${i} ran between ${start} and ${end}`);
}, 1000);
})
);
const returnValues = await Promise.all(threads);
console.log(returnValues.join("\n"));
// Output:
// Thread 0 ran between 2026-04-24T12:00:00 and 2026-04-24T12:00:01
// Thread 1 ran between 2026-04-24T12:00:00 and 2026-04-24T12:00:01
// Thread 2 ran between 2026-04-24T12:00:00 and 2026-04-24T12:00:01
// Thread 3 ran between 2026-04-24T12:00:00 and 2026-04-24T12:00:01
// Thread 4 ran between 2026-04-24T12:00:00 and 2026-04-24T12:00:01
I mean, it's clearly syntactically preferable to the Promise jungle of JavaScript, no?
That said, there are quite a few fundamental things that one needs to be aware of to build the right mental model of what to do and what not to do with threads in Ruby. The main one is that Ruby threads are limited by the GIL (global interpreter lock), which prevents them from executing Ruby code in parallel. This means that, similar to JavaScript, Ruby doesn't achieve multi-core parallelism with its threads, but rather concurrency.
Also, like JavaScript, the benefits of concurrency in Ruby are essentially limited to IO-bound tasks. IO-bound tasks are those that spend time waiting on external resources, such as network requests, file system operations or database queries. By using threads for these tasks, we allow other parts of our programs to continue running while waiting for the IO operations to complete.
Here's how we could use threads to perform some HTTP requests concurrently:
require "net/http"
require "json"
urls = %w[
https://jsonplaceholder.typicode.com/posts/1
https://jsonplaceholder.typicode.com/posts/2
https://jsonplaceholder.typicode.com/posts/3
]
posts = urls
.map { |url| Thread.new { Net::HTTP.get(URI(url)) } }
.map(&:value)
.map(&JSON.method(:parse))
# => [{ "id" => 1, ... }, { "id" => 2, ... }, { "id" => 3, ... }]
const urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3",
];
const posts = await Promise.all(
urls.map(
(url) => fetch(url).then(
(response) => response.json()
)
)
);
// => [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]
We'll look at more ways to make use of threads in future posts.
History
Thread has been part of Ruby since its inception in 1995.
In version 1.9.0, released in 2007, Ruby switched from green threads to a new thread implementation based on native OS threads, which is what we have today.