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

Too many writer threads

Error message

Too many writer threads

What it means

Concurrent::ReadWriteLock stores waiting writers in the middle bits of its state integer (WAITING_WRITER = 1 << 15, RUNNING_WRITER = 1 << 29); when the waiting-writer count saturates, acquire_write_lock raises Concurrent::ResourceLimitError. Waiting writers drain as soon as the lock frees, so saturation means writers are permanently stuck — classically a deadlock where a reader or the running writer never releases. Note the class documentation explicitly warns that acquiring the write lock while holding the read lock deadlocks this lock.

Source

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

          if waiting_writer?(c) && running_readers(c) == 1
            @WriteLock.signal
          end
          break
        end
      end
      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
      while true
        c = @Counter.value
        raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)

        if c == 0 # no readers OR writers running
          # if we successfully swap the RUNNING_WRITER bit on, then we can go ahead
          break if @Counter.compare_and_set(0, RUNNING_WRITER)
        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, OR another writer could increment
            @WriteLock.wait_until do
              # So we have to do another check inside the synchronized section
              # If a writer OR reader is running, then go to sleep
              c = @Counter.value
              !running_writer?(c) && !running_readers?(c)
            end

            # We just came out of a wait
            # If we successfully turn the RUNNING_WRITER bit on with an atomic swap,

View on GitHub (pinned to 0b88d5ff75)

Solutions

  1. Never take the write lock while holding the read lock on ReadWriteLock — take the write lock once for the whole read-modify-write, or use a design that does not need upgrading.
  2. Always release via `lock.with_write_lock { ... }` or begin/ensure pairing.
  3. When this error appears, dump all thread backtraces (Thread.list each backtrace) — the first stuck holder is the root cause, later queuers are victims.
  4. Keep write critical sections short and free of blocking IO so the waiting queue drains quickly.

Example fix

// before — read-then-write upgrade deadlocks this lock class
lock.with_read_lock do
  lock.with_write_lock { cache.write(x) }  # waits for its own read lock; writers pile up
end

// after — one exclusive acquisition for read-modify-write
lock.with_write_lock do
  v = cache.read
  cache.write(transform(v))
end
Defensive patterns

Strategy: try-catch

Validate before calling

Thread.list.each { |t| next if t == Thread.current } # survey holders before piling on writers

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: A thread holds the read lock and then tries to acquire the write lock on the same non-reentrant ReadWriteLock while other writers queue behind it; a writer dies or raises between manual acquire_write_lock and release_write_lock; read locks leaked so the running writer never finishes and waiting writers accumulate.

Common situations: Read-then-write upgrade patterns on a lock documented as non-upgradeable; skipping release on exception paths; background jobs piling onto a wedged lock until the bitfield fills.

Related errors


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