ruby-concurrency/concurrent-ruby · error · Concurrent::ResourceLimitError

Too many reader threads

Error message

Too many reader threads

What it means

Concurrent::ReadWriteLock packs its entire state into one AtomicFixnum: the low 15 bits count active readers, capping concurrent read locks at MAX_READERS = 32767. acquire_read_lock raises Concurrent::ResourceLimitError when that bitfield is full, protecting the packed representation. A healthy application almost never has 32k simultaneous readers, so this error usually indicates leaked read locks.

Source

Thrown at lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb:116

      acquire_write_lock
      begin
        yield
      ensure
        release_write_lock
      end
    end

    # Acquire a read lock. If a write lock has been acquired will block until
    # it is released. Will not block if other read locks have been acquired.
    #
    # @return [Boolean] true if the lock is successfully acquired
    #
    # @raise [Concurrent::ResourceLimitError] if the maximum number of readers
    #   is exceeded.
    def acquire_read_lock
      while true
        c = @Counter.value
        raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)

        # If a writer is waiting when we first queue up, we need to wait
        if waiting_writer?(c)
          @ReadLock.wait_until { !waiting_writer? }

          # after a reader has waited once, they are allowed to "barge" ahead of waiting writers
          # but if a writer is *running*, the reader still needs to wait (naturally)
          while true
            c = @Counter.value
            if running_writer?(c)
              @ReadLock.wait_until { !running_writer? }
            else
              return if @Counter.compare_and_set(c, c+1)
            end
          end
        else
          break if @Counter.compare_and_set(c, c+1)
        end

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Switch to the block form `lock.with_read_lock { ... }` — its ensure clause always releases, even on exceptions.
  2. If manual pairing is required, wrap it: acquire_read_lock; begin ... ensure release_read_lock end.
  3. Audit code between acquire and release for early returns, throws, or re-raises that skip the release.
  4. Instrument acquire/release (log a counter) to identify which caller leaks read locks.
  5. If you legitimately exceed 32767 concurrent readers, shard the data across multiple ReadWriteLock instances.

Example fix

// before
lock.acquire_read_lock
rows = read_table   # raise here leaks the lock forever
lock.release_read_lock

// after
lock.with_read_lock { read_table }
Defensive patterns

Strategy: try-catch

Try / catch

begin
  lock.with_read_lock { read_table }
rescue Concurrent::ResourceLimitError => e
  logger.fatal("read-lock saturation, probable read-lock leak: #{e.message}")
  raise
end

Prevention

When it happens

Trigger: Calling acquire_read_lock without a matching release_read_lock and then acquiring again (loop iterations, threads, or requests each add one to the count); an exception raised between manual acquire and release so release never runs; long-lived threads holding read locks forever.

Common situations: Using the manual acquire/release API instead of with_read_lock; early return or throw between acquire and release; a rescue-and-reraise path that skips release; treating a read lock as a registration mechanism and never releasing it.

Related errors


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