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)
endView on GitHub (pinned to 0b88d5ff75)
Solutions
- Switch to the block form `lock.with_read_lock { ... }` — its ensure clause always releases, even on exceptions.
- If manual pairing is required, wrap it: acquire_read_lock; begin ... ensure release_read_lock end.
- Audit code between acquire and release for early returns, throws, or re-raises that skip the release.
- Instrument acquire/release (log a counter) to identify which caller leaks read locks.
- 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
- Never call acquire_read_lock without an ensure-guarded release_read_lock; prefer with_read_lock.
- Do not hold read locks across long waits (IO, sleeps, queue pops) — readers accumulate.
- Treat ResourceLimitError as a leak detector, not a transient condition; do not blindly retry.
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
- Too many reader threads
- Too many writer threads
- no block given
- Too many reader holds on this thread
- Too many writer threads
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/ed7a8caca8932831.
Report an issue: GitHub.