ruby-concurrency/concurrent-ruby · error · Concurrent::RejectedExecutionError
Concurrent::RejectedExecutionError
Error message
Concurrent::RejectedExecutionError
What it means
ThreadPoolExecutor (and SingleThreadExecutor) use a bounded work queue; once the queue holds max_queue tasks and no new thread can be created, the executor runs its fallback action. With the default fallback_policy :abort that action raises Concurrent::RejectedExecutionError (a Concurrent::Error subclass, errors.rb:48) from the posting thread, as built at abstract_executor_service.rb:88. Posting to an executor that is shut down rejects the same way. This is Java-style explicit backpressure: the producer learns immediately that its task was not accepted.
Source
Thrown at lib/concurrent-ruby/concurrent/executor/abstract_executor_service.rb:88
# @!macro executor_service_method_auto_terminate_setter
def auto_terminate=(value)
deprecated "Method #auto_terminate= has no effect. Set :auto_terminate option when executor is initialized."
end
private
# Returns an action which executes the `fallback_policy` once the queue
# size reaches `max_queue`. The reason for the indirection of an action
# is so that the work can be deferred outside of synchronization.
#
# @param [Array] args the arguments to the task which is being handled.
#
# @!visibility private
def fallback_action(*args)
case fallback_policy
when :abort
lambda { raise RejectedExecutionError }
when :discard
lambda { false }
when :caller_runs
lambda {
begin
yield(*args)
rescue => ex
# let it fail
log DEBUG, ex
end
true
}
else
lambda { fail "Unknown fallback policy #{fallback_policy}" }
end
end
def ns_execute(*args, &task)View on GitHub (pinned to 0b88d5ff75)
Solutions
- Rescue Concurrent::RejectedExecutionError at the submit site and apply backpressure (retry with backoff, run inline, or drop).
- Raise max_queue (0 means unbounded on the pure-Ruby implementation) and/or raise max_threads to absorb bursts.
- Set fallback_policy: :caller_runs so the producer thread executes overflow work itself, or :discard to drop silently.
- Check executor.running? before posting and coordinate shutdown so late submitters fail in your code, not inside the pool.
Example fix
# before
pool = Concurrent::ThreadPoolExecutor.new(min_threads: 1, max_threads: 2, max_queue: 2, fallback_policy: :abort)
100.times { |i| pool.post { heavy(i) } } # 3rd pending task raises RejectedExecutionError
# after
pool = Concurrent::ThreadPoolExecutor.new(min_threads: 1, max_threads: 8, max_queue: 1_000, fallback_policy: :caller_runs)
100.times { |i| pool.post { heavy(i) } } Defensive patterns
Strategy: try-catch
Validate before calling
def can_submit?(pool) return false unless pool.running? return true unless pool.respond_to?(:remaining_capacity) cap = pool.remaining_capacity # -1 means unbounded queue cap == -1 || cap.positive? end
Try / catch
begin
pool.post { work }
rescue Concurrent::RejectedExecutionError
work.call # caller-runs fallback, or retry with backoff
end Prevention
- Size max_queue for worst-case bursts, not average load.
- Prefer :caller_runs over :abort when losing tasks is worse than slowing producers.
- Route all submissions through one guarded helper so rejection handling is uniform.
- Never post after shutdown; track the executor lifecycle explicitly.
When it happens
Trigger: Concurrent::ThreadPoolExecutor.new(min_threads: 1, max_threads: 1, max_queue: 5, fallback_policy: :abort) then posting 6+ long-running blocks; a burst of submissions while all threads are busy; calling #post/#<< after #shutdown when the executor is no longer running.
Common situations: Sizing a bounded pool for a bursty job queue; migrating from the effectively unbounded global_io_executor to a strict custom pool; background or timeout paths that still submit tasks during shutdown; handler code posting to a shared bounded pool under load.
Related errors
- number of threads must be greater than zero
- `synchronous` cannot be set unless `max_queue` is 0
- `max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}
- `max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE
- `min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/747e1ef8776fba3c.
Report an issue: GitHub.