ruby-concurrency/concurrent-ruby · error · ArgumentError
`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}
Error message
`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE} What it means
Raised by RubyThreadPoolExecutor#ns_initialize when the :min_threads option, after `.to_i` coercion, is less than DEFAULT_MIN_POOL_SIZE (which is 0 in this version of concurrent-ruby). The pool cannot honor a request to keep fewer than zero threads alive, so construction fails immediately with ArgumentError. Because the value is fetched with `opts.fetch(:min_threads, 0).to_i`, any object that coerces to a negative Integer (e.g. -1, -2.9, "-5") triggers it.
Source
Thrown at lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:158
deprecated "#prune_pool has no effect and will be removed in next the release, see https://github.com/ruby-concurrency/concurrent-ruby/pull/1082."
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
View on GitHub (pinned to 0b88d5ff75)
Solutions
- Set min_threads to 0 or a positive Integer, or omit it entirely (the default is 0, which creates threads on demand)
- Validate the value at the config boundary: parse with Integer(...) and reject anything below 0 with a clear configuration error before constructing the pool
- If the value arrives as a string, coerce and range-check it explicitly instead of relying on the library's internal .to_i
Example fix
# before
pool = Concurrent::ThreadPoolExecutor.new(min_threads: ENV.fetch('MIN_THREADS').to_i)
# after
min = Integer(ENV.fetch('MIN_THREADS', 0))
raise ArgumentError, "MIN_THREADS must be >= 0, got #{min}" if min < 0
pool = Concurrent::ThreadPoolExecutor.new(min_threads: min) Defensive patterns
Strategy: validation
Validate before calling
min = Integer(ENV.fetch('MIN_THREADS', 0))
raise ArgumentError, "min_threads must be >= 0" if min < 0
pool = Concurrent::ThreadPoolExecutor.new(min_threads: min) Type guard
->(v) { v.is_a?(Integer) && v >= 0 } Try / catch
begin
Concurrent::ThreadPoolExecutor.new(min_threads: min)
rescue ArgumentError => e
raise ConfigError, "thread pool config rejected: #{e.message}"
end Prevention
- Centralize pool construction in one factory so size validation lives in a single place
- Parse numeric config with Integer(...) at load time, not with to_i at use time
- Add config schema checks (range assertions) that run at boot, not on first request
When it happens
Trigger: Concurrent::ThreadPoolExecutor.new(min_threads: -1); passing a numeric string like min_threads: '-5' (String#to_i keeps the sign); computing the value from config such as worker_count - 2 when worker_count is 1; floats like -1.5 (to_i gives -1).
Common situations: Config-driven pool sizing where min_threads comes from ENV vars or YAML without range validation; formulas that can go negative under load or small deployments; teams porting Java ThreadPoolExecutor code where negative values were silently clamped.
Related errors
- `min_threads` cannot be more than `max_threads`
- 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/2e40e6cb953fa4f1.
Report an issue: GitHub.