[Proposal] Limit concurrency for specific async query workloads

Motivation

Some workloads need lower concurrency to avoid excessive database load and query contention. Since global_executor_concurrency applies to all async queries, applications need a way to limit concurrency for a specific workload without affecting unrelated async work.

Current workaround

Applications can limit concurrency by processing queries in batches:

queries.each_slice(2).flat_map do |batch|
  batch.map { |query| query.async_pluck(:id) }.map(&:value)
end

However, the next batch cannot start until every query in the current batch completes. If one query is slow, available executor capacity remains unused. Avoiding this requires application-level completion tracking or a custom scheduler.

Proposed API

Add a block-based API that applies a concurrency limit to async queries scheduled within the block:

promises = ActiveRecord::Base.with_async_query_concurrency(2) do
  queries.map { |query| query.async_pluck(:id) }
end

results = promises.map(&:value)

All async queries scheduled within the block share the same concurrency limit.

Behavior

  • Scheduling an async query remains non-blocking.

  • At most the specified number of queries are admitted to the executor concurrently.

  • Additional queries wait in the workload’s limiter until a slot becomes available.

  • Queries outside the block are unaffected.

  • The global executor remains shared and its concurrency limit still applies.

  • A slot is released when a query completes or raises an error.

Example

An application may keep:

config.active_record.global_executor_concurrency = 8

while limiting one database-heavy workload:

ActiveRecord::Base.with_async_query_concurrency(2) do
  reports.map(&:load_async)
end

This allows unrelated async queries to continue using the executor’s remaining capacity.

1 Like