{"record":{"id":"747e1ef8776fba3c","repo":"ruby-concurrency/concurrent-ruby","slug":"concurrent-rejectedexecutionerror","errorCode":null,"errorMessage":"Concurrent::RejectedExecutionError","messagePattern":"Concurrent::RejectedExecutionError","errorType":"exception","errorClass":"Concurrent::RejectedExecutionError","httpStatus":null,"severity":"error","filePath":"lib/concurrent-ruby/concurrent/executor/abstract_executor_service.rb","lineNumber":88,"sourceCode":"\n    # @!macro executor_service_method_auto_terminate_setter\n    def auto_terminate=(value)\n      deprecated \"Method #auto_terminate= has no effect. Set :auto_terminate option when executor is initialized.\"\n    end\n\n    private\n\n    # Returns an action which executes the `fallback_policy` once the queue\n    # size reaches `max_queue`. The reason for the indirection of an action\n    # is so that the work can be deferred outside of synchronization.\n    #\n    # @param [Array] args the arguments to the task which is being handled.\n    #\n    # @!visibility private\n    def fallback_action(*args)\n      case fallback_policy\n      when :abort\n        lambda { raise RejectedExecutionError }\n      when :discard\n        lambda { false }\n      when :caller_runs\n        lambda {\n          begin\n            yield(*args)\n          rescue => ex\n            # let it fail\n            log DEBUG, ex\n          end\n          true\n        }\n      else\n        lambda { fail \"Unknown fallback policy #{fallback_policy}\" }\n      end\n    end\n\n    def ns_execute(*args, &task)","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/ruby-concurrency/concurrent-ruby/blob/0b88d5ff75f69b3740c8f0868e76f833cb2fd45d/lib/concurrent-ruby/concurrent/executor/abstract_executor_service.rb#L70-L106","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\npool = Concurrent::ThreadPoolExecutor.new(min_threads: 1, max_threads: 2, max_queue: 2, fallback_policy: :abort)\n100.times { |i| pool.post { heavy(i) } } # 3rd pending task raises RejectedExecutionError\n\n# after\npool = Concurrent::ThreadPoolExecutor.new(min_threads: 1, max_threads: 8, max_queue: 1_000, fallback_policy: :caller_runs)\n100.times { |i| pool.post { heavy(i) } }","handlingStrategy":"try-catch","validationCode":"def can_submit?(pool)\n  return false unless pool.running?\n  return true unless pool.respond_to?(:remaining_capacity)\n  cap = pool.remaining_capacity # -1 means unbounded queue\n  cap == -1 || cap.positive?\nend","typeGuard":null,"tryCatchPattern":"begin\n  pool.post { work }\nrescue Concurrent::RejectedExecutionError\n  work.call # caller-runs fallback, or retry with backoff\nend","preventionTips":["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."],"tags":["concurrency","ruby","thread-pool","backpressure","executor-queue"],"backgroundTag":"thread-pool-saturation","analyzedSha":"0b88d5ff75f69b3740c8f0868e76f833cb2fd45d","analyzedAt":"2026-08-21T20:12:56.291Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}