ruby-concurrency/concurrent-ruby · error · ArgumentError
`min_threads` cannot be more than `max_threads`
Error message
`min_threads` cannot be more than `max_threads`
What it means
Raised by RubyThreadPoolExecutor#ns_initialize when min_threads exceeds max_threads after `.to_i` coercion. A pool that must keep more threads alive than it is allowed to create is contradictory, so construction fails with ArgumentError. Since the default max_threads is 2_147_483_647, this only fires when max_threads is explicitly set below min_threads.
Source
Thrown at lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:159
end
private
# @!visibility private
def ns_initialize(opts)
@min_length = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i
@max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
@idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
@max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
@synchronous = opts.fetch(:synchronous, DEFAULT_SYNCHRONOUS)
@fallback_policy = opts.fetch(:fallback_policy, :abort)
raise ArgumentError.new("`synchronous` cannot be set unless `max_queue` is 0") if @synchronous && @max_queue > 0
raise ArgumentError.new("#{@fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICIES.include?(@fallback_policy)
raise ArgumentError.new("`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if @max_length < DEFAULT_MIN_POOL_SIZE
raise ArgumentError.new("`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}") if @max_length > DEFAULT_MAX_POOL_SIZE
raise ArgumentError.new("`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if @min_length < DEFAULT_MIN_POOL_SIZE
raise ArgumentError.new("`min_threads` cannot be more than `max_threads`") if min_length > max_length
@pool = [] # all workers
@ready = [] # used as a stash (most idle worker is at the start)
@queue = [] # used as queue
# @ready or @queue is empty at all times
@scheduled_task_count = 0
@completed_task_count = 0
@largest_length = 0
@workers_counter = 0
@ruby_pid = $$ # detects if Ruby has forked
end
# @!visibility private
def ns_limited_queue?
@max_queue != 0
end
# @!visibility privateView on GitHub (pinned to 0b88d5ff75)
Solutions
- Make max_threads at least min_threads (e.g. min_threads: 4, max_threads: 8)
- Drop max_threads entirely if you only need a floor — the default allows essentially unlimited growth
- Drop min_threads if you only need a ceiling — the default of 0 lets the pool shrink to zero idle workers
Example fix
# before pool = Concurrent::ThreadPoolExecutor.new(min_threads: 8, max_threads: 4) # after pool = Concurrent::ThreadPoolExecutor.new(min_threads: 4, max_threads: 8)
Defensive patterns
Strategy: validation
Validate before calling
min = Integer(opts.fetch(:min_threads, 0))
max = Integer(opts.fetch(:max_threads, Concurrent::ThreadPoolExecutor::DEFAULT_MAX_POOL_SIZE))
raise ArgumentError, "min_threads (#{min}) must be <= max_threads (#{max})" if min > max
pool = Concurrent::ThreadPoolExecutor.new(min_threads: min, max_threads: max) Type guard
->(mn, mx) { mn.is_a?(Integer) && mx.is_a?(Integer) && mn >= 0 && mn <= mx } Try / catch
begin
Concurrent::ThreadPoolExecutor.new(min_threads: min, max_threads: max)
rescue ArgumentError => e
raise ConfigError, "thread pool config rejected: #{e.message}"
end Prevention
- Derive max from min in config (max = [min, desired_max].max) so the invariant cannot break
- Keep min/max adjacent in YAML/ENV and validate them together at boot
- Fail fast on config errors during startup rather than rescuing into silent defaults
When it happens
Trigger: Concurrent::ThreadPoolExecutor.new(min_threads: 10, max_threads: 5); sidekiq/puma-style config files where min and max are tuned separately; min_threads taken from ENV but max_threads hardcoded lower; copy-paste of pool settings between apps with different sizing conventions.
Common situations: Tuning concurrency settings during capacity planning; mismatched configs edited by different people; autoscaling configs where min scales with CPU count but max was fixed earlier; confusion with Java semantics where an IllegalArgumentException of similar shape exists.
Related errors
- `min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}
- 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
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/f97c563ebf22cc5c.
Report an issue: GitHub.