{"record":{"id":"f97c563ebf22cc5c","repo":"ruby-concurrency/concurrent-ruby","slug":"min-threads-cannot-be-more-than-max-threads-f97c56","errorCode":null,"errorMessage":"`min_threads` cannot be more than `max_threads`","messagePattern":"`min_threads` cannot be more than `max_threads`","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb","lineNumber":159,"sourceCode":"    end\n\n    private\n\n    # @!visibility private\n    def ns_initialize(opts)\n      @min_length      = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i\n      @max_length      = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i\n      @idletime        = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i\n      @max_queue       = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i\n      @synchronous     = opts.fetch(:synchronous, DEFAULT_SYNCHRONOUS)\n      @fallback_policy = opts.fetch(:fallback_policy, :abort)\n\n      raise ArgumentError.new(\"`synchronous` cannot be set unless `max_queue` is 0\") if @synchronous && @max_queue > 0\n      raise ArgumentError.new(\"#{@fallback_policy} is not a valid fallback policy\") unless FALLBACK_POLICIES.include?(@fallback_policy)\n      raise ArgumentError.new(\"`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}\") if @max_length < DEFAULT_MIN_POOL_SIZE\n      raise ArgumentError.new(\"`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}\") if @max_length > DEFAULT_MAX_POOL_SIZE\n      raise ArgumentError.new(\"`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}\") if @min_length < DEFAULT_MIN_POOL_SIZE\n      raise ArgumentError.new(\"`min_threads` cannot be more than `max_threads`\") if min_length > max_length\n\n      @pool                 = [] # all workers\n      @ready                = [] # used as a stash (most idle worker is at the start)\n      @queue                = [] # used as queue\n      # @ready or @queue is empty at all times\n      @scheduled_task_count = 0\n      @completed_task_count = 0\n      @largest_length       = 0\n      @workers_counter      = 0\n      @ruby_pid             = $$ # detects if Ruby has forked\n    end\n\n    # @!visibility private\n    def ns_limited_queue?\n      @max_queue != 0\n    end\n\n    # @!visibility private","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb#L141-L177","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\npool = Concurrent::ThreadPoolExecutor.new(min_threads: 8, max_threads: 4)\n\n# after\npool = Concurrent::ThreadPoolExecutor.new(min_threads: 4, max_threads: 8)","handlingStrategy":"validation","validationCode":"min = Integer(opts.fetch(:min_threads, 0))\nmax = Integer(opts.fetch(:max_threads, Concurrent::ThreadPoolExecutor::DEFAULT_MAX_POOL_SIZE))\nraise ArgumentError, \"min_threads (#{min}) must be <= max_threads (#{max})\" if min > max\npool = Concurrent::ThreadPoolExecutor.new(min_threads: min, max_threads: max)","typeGuard":"->(mn, mx) { mn.is_a?(Integer) && mx.is_a?(Integer) && mn >= 0 && mn <= mx }","tryCatchPattern":"begin\n  Concurrent::ThreadPoolExecutor.new(min_threads: min, max_threads: max)\nrescue ArgumentError => e\n  raise ConfigError, \"thread pool config rejected: #{e.message}\"\nend","preventionTips":["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"],"tags":["ruby","concurrency","thread-pool","argument-validation","configuration"],"backgroundTag":"thread-pool-misconfiguration","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}