ruby-concurrency/concurrent-ruby · error · ArgumentError

unbuffered channels cannot have a capacity

Error message

unbuffered channels cannot have a capacity

What it means

Concurrent::Channel.new builds the channel's buffer from the :buffer/:capacity options. An unbuffered channel is a pure rendezvous between sender and receiver with no storage, so a capacity is meaningless; passing both buffer: :unbuffered and a capacity (also accepted under the :size alias) is rejected with this ArgumentError.

Source

Thrown at lib/concurrent-ruby-edge/concurrent/channel.rb:58

    def_delegators :buffer,
      :size, :capacity, :close, :closed?,
      :blocking?, :empty?, :full?

    alias_method :length, :size
    alias_method :stop, :close

    def initialize(opts = {})
      # undocumented -- for internal use only
      if opts.is_a? Buffer::Base
        self.buffer = opts
        return
      end

      capacity = opts[:capacity] || opts[:size]
      buffer = opts[:buffer]

      if capacity && buffer == :unbuffered
        raise ArgumentError.new('unbuffered channels cannot have a capacity')
      elsif capacity.nil? && buffer.nil?
        self.buffer = BUFFER_TYPES[:unbuffered].new
      elsif capacity == 0 && buffer == :buffered
        self.buffer = BUFFER_TYPES[:unbuffered].new
      elsif buffer == :unbuffered
        self.buffer = BUFFER_TYPES[:unbuffered].new
      elsif capacity.nil? || capacity < 1
        raise ArgumentError.new('capacity must be at least 1 for this buffer type')
      else
        buffer ||= :buffered
        self.buffer = BUFFER_TYPES[buffer].new(capacity)
      end

      self.validator = opts.fetch(:validator, DEFAULT_VALIDATOR)
    end

    def put(item)
      return false unless validate(item, false, false)

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Drop the capacity: Concurrent::Channel.new(buffer: :unbuffered), or simply Concurrent::Channel.new which is unbuffered by default.
  2. If you actually want bounded storage, use buffer: :buffered (or :dropping/:sliding) with a capacity of at least 1.
  3. Validate channel option hashes once at config-load time instead of deep inside worker code.

Example fix

# before
ch = Concurrent::Channel.new(buffer: :unbuffered, capacity: 10)

# after
ch = Concurrent::Channel.new(buffer: :unbuffered)
# or, if capacity is actually wanted:
ch = Concurrent::Channel.new(buffer: :buffered, capacity: 10)
Defensive patterns

Strategy: validation

Validate before calling

def channel_opts_conflict?(opts)
  (opts[:capacity] || opts[:size]) && opts[:buffer] == :unbuffered
end

raise ArgumentError, 'drop :capacity when buffer is :unbuffered' if channel_opts_conflict?(opts)
ch = Concurrent::Channel.new(**opts)

Try / catch

begin
  ch = Concurrent::Channel.new(**opts)
rescue ArgumentError => e
  raise ConfigError, "invalid channel options: #{e.message}"
end

Prevention

When it happens

Trigger: Concurrent::Channel.new(buffer: :unbuffered, capacity: 10), or via the alias Concurrent::Channel.new(size: 3, buffer: :unbuffered). Any truthy capacity together with buffer: :unbuffered triggers it.

Common situations: Switching a channel from buffered to unbuffered in a shared config without removing the capacity key; YAML/ENV-driven channel options where capacity is defaulted for every channel; copy-pasting option hashes between channel definitions.

Related errors


AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21). Data as JSON: /api/errors/d89389d2dfc27ed6. Report an issue: GitHub.