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.