ruby-concurrency/concurrent-ruby · error · Concurrent::TimeoutError
Concurrent::TimeoutError
Error message
Concurrent::TimeoutError
What it means
`Agent#await_for!(timeout)` blocks until every action dispatched so far to the agent (from the current thread or nested by the agent) has completed, or the timeout elapses. The bang variant raises Concurrent::TimeoutError when `wait(timeout)` returns false — the action queue did not drain in time — while the non-bang `await_for(timeout)` returns false instead. Timeouts almost always mean the agent's executor is saturated, actions are slow or blocking, or the dispatch chain is longer than the budget assumes.
Solutions
- Switch to the boolean form and branch on it: `agent.await_for(5)` returns true/false instead of raising.
- Increase or remove the timeout (`await` waits indefinitely) when convergence is guaranteed.
- Profile the dispatched actions — move blocking I/O or heavy computation out of agent actions into tasks.
- Check for self-deadlock: an action (or its nested dispatches) must not wait on the same agent's completion.
Example fix
// before agent.await_for!(0.1) # raises Concurrent::TimeoutError under load // after unless agent.await_for(5) logger.warn 'agent did not settle within 5s' end
Defensive patterns
Strategy: try-catch
Try / catch
begin agent.await_for!(5) rescue Concurrent::TimeoutError # queue not drained; log, extend the budget, or degrade gracefully end
Prevention
- Prefer boolean await_for over the bang variant in production code.
- Keep agent actions short and non-blocking.
- Never wait on an agent from inside its own action chain.
- Size timeouts from measured action cost, not constants.
When it happens
Trigger: `agent.await_for!(0.1)` right after dispatching expensive actions; awaiting from inside the agent's own action chain in a way that only the agent can complete (self-deadlock); a starved shared executor (`Concurrent.global_fast_executor`) shared with other heavy work.
Common situations: Test suites asserting agent convergence with tight timeouts; bulk updates to one agent in a loop then awaiting; actions performing I/O or sleeping on the agent's thread pool.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- agent is not failed
- Concurrent::Agent::ValidationError
- no action given
- timeout must 0.0 or more
- unrecognized error mode
AI-assisted analysis of ruby-concurrency/concurrent-ruby@0b88d5ff75 (2026-08-21).
Data as JSON: /api/errors/506508558adf300c.
Report an issue: GitHub.
Appendix: source
Thrown at lib/concurrent-ruby/concurrent/agent.rb:378
# @return [Boolean] true if all actions complete before timeout else false
#
# @!macro agent_await_warning
def await_for(timeout)
wait(timeout.to_f)
end
# Blocks the current thread until all actions dispatched thus far, from this
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
# has elapsed.
#
# @param [Float] timeout the maximum number of seconds to wait
# @return [Boolean] true if all actions complete before timeout
#
# @raise [Concurrent::TimeoutError] when timeout is reached
#
# @!macro agent_await_warning
def await_for!(timeout)
raise Concurrent::TimeoutError unless wait(timeout.to_f)
true
end
# Blocks the current thread until all actions dispatched thus far, from this
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
# has elapsed. Will block indefinitely when timeout is nil or not given.
#
# Provided mainly for consistency with other classes in this library. Prefer
# the various `await` methods instead.
#
# @param [Float] timeout the maximum number of seconds to wait
# @return [Boolean] true if all actions complete before timeout else false
#
# @!macro agent_await_warning
def wait(timeout = nil)
latch = Concurrent::CountDownLatch.new(1)
enqueue_await_job(latch)
latch.wait(timeout)View on GitHub (pinned to 0b88d5ff75)