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.valueView on GitHub (pinned to 0b88d5ff75)
Solutions
- Use with_write_lock { } for every write critical section so release is guaranteed.
- Ensure read locks on the same data are released — otherwise queued writers never run (see the reader limit errors).
- When the error appears, dump all thread backtraces; the first stuck holder is the cause, later queuers are symptoms.
- 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
- Use with_write_lock for every write section so release is guaranteed.
- Make sure read locks on the same data are released, or no writer will ever run.
- Keep write sections short and free of blocking IO so the waiting-writer queue drains.
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
- Too many writer threads
- Too many reader holds on this thread
- Too many reader threads
- Too many reader threads
- no block given
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/becae73adebfbd5f.
Report an issue: GitHub.