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

Too many writer threads

Error message

Too many writer threads

What it means

ReentrantReadWriteLock reserves 14 middle bits of its @Counter for waiting writers; when that field saturates at MAX_WRITERS, acquire_write_lock raises Concurrent::ResourceLimitError. Waiting writers only drain when the running writer releases and readers clear, so saturation means the lock is wedged — a writer that never released, or readers whose leak prevents any writer from running.

Source

Thrown at lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb:273

      true
    end

    # Acquire a write lock. Will block and wait for all active readers and writers.
    #
    # @return [Boolean] true if the lock is successfully acquired
    #
    # @raise [Concurrent::ResourceLimitError] if the maximum number of writers
    #   is exceeded.
    def acquire_write_lock
      if (held = @HeldCount.value) >= WRITE_LOCK_HELD
        # if we already have a write (exclusive) lock, there's no need to wait
        @HeldCount.value = held + WRITE_LOCK_HELD
        return true
      end

      while true
        c = @Counter.value
        raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)

        # To go ahead and take the lock without waiting, there must be no writer
        #   running right now, AND no writers who came before us still waiting to
        #   acquire the lock
        # Additionally, if any read locks have been taken, we must hold all of them
        if held > 0 && @Counter.compare_and_set(1, c+RUNNING_WRITER)
          # If we are the only one reader and successfully swap the RUNNING_WRITER bit on, then we can go ahead
          @HeldCount.value = held + WRITE_LOCK_HELD
          return true
        elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
          while true
            # Now we have successfully incremented, so no more readers will be able to increment
            #   (they will wait instead)
            # However, readers OR writers could decrement right here
            @WriteQueue.synchronize do
              # So we have to do another check inside the synchronized section
              # If a writer OR another reader is running, then go to sleep
              c = @Counter.value

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Use with_write_lock { } for every write critical section so release is guaranteed.
  2. Ensure read locks on the same data are released — otherwise queued writers never run (see the reader limit errors).
  3. When the error appears, dump all thread backtraces; the first stuck holder is the cause, later queuers are symptoms.
  4. Keep write critical sections short and free of blocking IO.

Example fix

// before — holder dies, every other writer queues forever
lock.acquire_write_lock
persist(rows)      # raises, release never runs
lock.release_write_lock

// after
lock.with_write_lock { persist(rows) }
Defensive patterns

Strategy: try-catch

Try / catch

begin
  lock.with_write_lock { persist(rows) }
rescue Concurrent::ResourceLimitError => e
  Thread.list.each { |t| logger.error("#{t}: #{(t.backtrace || []).join(' | ')}") }
  raise
end

Prevention

When it happens

Trigger: Manual acquire_write_lock without begin/ensure release where an exception kills the holder while others queue; a leaked read lock preventing queued writers from ever proceeding; heavy write traffic funneled through a lock whose current writer is deadlocked.

Common situations: Write critical sections containing early throws; mixing manual and block lock APIs; long write batches under a single lock while background jobs pile up behind it.

Related errors


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