I Had No Idea Rails Could Query the Database in Parallel

June 03, 2026 · 4 min read

I Had No Idea Rails Could Query the Database in Parallel

Every Rails controller I've ever written runs database queries sequentially. Load the user, then load their orders, then count notifications, then fetch the dashboard stats. Each one waits for the previous to finish before the next one starts. I never questioned it — until I found load_async.

What load_async Does

load_async fires a query in a background thread and returns immediately. The results are ready when you need them. If you have multiple independent queries, they can all run at the same time instead of waiting in line.

ruby
class DashboardController < ApplicationController
  def show
    @orders = current_user.orders.where(status: :active).load_async
    @notifications = current_user.notifications.unread.load_async
    @stats = Order.where(created_at: 30.days.ago..).group(:status).async_count

    # All three queries are running concurrently at this point.
    # When the view renders and calls @orders.each, it blocks
    # only if the query hasn't finished yet.
  end
end

Without load_async, those three queries run back-to-back. If each takes 50ms, that's 150ms total. With load_async, they overlap — total time is closer to 50ms (the slowest one). On a dashboard with five or six queries, the difference is real.

Async Aggregation Helpers

Rails doesn't stop at loading records. There's a full set of async helpers for aggregations:

ruby
total_revenue  = Order.where(status: :placed).async_sum(:amount)
active_count   = User.where(status: :active).async_count
oldest_order   = Order.async_minimum(:created_at)
avg_rating     = Review.async_average(:score)
recent_ids     = Order.where(created_at: 1.week.ago..).async_ids

Each one returns an ActiveRecord::Promise. You call .value to get the result — and it blocks only if the query hasn't finished yet. Fire them all at the top of the action, do other work, and collect the values when you need them.

Configuration

Async queries don't work out of the box. You need to configure a thread pool executor. In config/application.rb:

ruby
config.active_record.async_query_executor = :global_thread_pool
config.active_record.global_executor_concurrency = 4

The :global_thread_pool shares threads across all async queries in the app. There's also :multi_thread_pool if you need per-database configuration — useful if you have read replicas or multiple databases.

One gotcha: your database connection pool needs to be large enough to handle the extra concurrent connections. If you set global_executor_concurrency to 4 but your pool in database.yml is only 5, you'll run into contention under load. A safe starting point is setting the pool size to your Puma thread count plus the async executor concurrency.

When NOT to Use It

Not every query benefits from load_async. It adds overhead — thread scheduling, connection checkout, synchronization — that only pays off when queries are slow enough and independent enough to justify running in parallel.

Skip it when:

Queries are already fast. If each query takes 2-3ms, the thread pool overhead might cost more than you save. The gains show up when individual queries take 20ms+.

Queries depend on each other. If query B needs the result of query A, you can't run them in parallel. load_async only helps with independent queries.

You're in a transaction. Async queries run on separate connections, which means they're outside the current transaction. If you need transactional consistency, stick with synchronous.

Your connection pool is tight. Each async query checks out its own connection. On a busy app with an undersized pool, this can cause connection timeouts instead of speed improvements.

The Takeaway

load_async has been sitting in Rails since 7.0, hiding in plain sight. For dashboard-heavy pages where you're loading several unrelated collections, the improvement is almost free — a few method calls and suddenly your sequential 200ms becomes a concurrent 60ms.

The key insight: it doesn't make individual queries faster. It makes groups of independent queries faster by running them at the same time. Simple idea, significant impact on pages that load a lot of data.

Leave a Comment

Your email will not be published.