Ruby timeouts

Chaotic delays, explicit cutoffs.

A library for timeouts in Ruby that prefers cooperative cancellation and reserves kill switches when you opt-in.

Safety

Why skip Timeout.timeout?

Ruby’s built-in timeout can yell “time’s up!” from a background thread on almost any line. TIMEx waits until you call check!, so you only stop where it is actually safe.

stdlib — surprise guest
require "timeout"

Timeout.timeout(2) do
  mutex.synchronize do
    # the timer thread can raise right here
    write_half_a_buffer
  end
end
That raise can land between two random steps—inside a lock, halfway through IO, or deep in native code. Fun for debugging, bad for production.
timex — you pick the pit stops
require "timex"

TIMEx.call(2.0) do |d|
  1000.times do
    d.check!   # cheap check: any time left?
    process_row
  end
end
Expired is an Exception, not a plain StandardError, so a wide rescue => e will not quietly eat your timeout.
Strategies

Pick the timeout flavor that fits the job.

Most paths take a Deadline—a shared clock that says “wrap it up.” Ask for the shortest time you need with min, pass the same clock downstream with headers, and reach for the bigger hammers only when Ruby alone cannot keep the promise.

Tiny recipes

Copy something that fits your day.

Same gem, four moods: loop in Ruby, wait on a socket, stack a polite timeout with a backup plan, or thread the deadline through Rack. Hop between the tabs, peek at the comments—no quiz at the end.

playground/ TIMEx
require "timex"

# Whole block gets 2 seconds; `deadline` is your little stopwatch.
result = TIMEx.call(2.0) do |deadline|
  rows.each do |row|
    deadline.check!   # cheap “still got time?” poke
    process(row)
  end
end
# Read up to 4096 bytes, but bail if this one read takes more than 1s.
TIMEx::Strategies::IO.read(socket, 4096, deadline: 1.0)
# Ask nicely first (cooperative). If things stall, subprocess after grace.
# idempotent: true is required—the block may run twice on escalation.
TIMEx::Composers::TwoPhase.new(
  soft: :cooperative,
  hard: :subprocess,
  grace: 0.5,
  hard_deadline: 1.0,
  idempotent: true
).call(deadline: 5.0) { run_user_code }
# Gemfile: gem "timex"
# config.ru — teach Rack about incoming deadlines
use TIMEx::Propagation::RackMiddleware

# When you call another service, tuck the deadline into the headers
TIMEx::Propagation::HttpHeader.inject(headers, deadline)
Design

Feels obvious once you use it.

Sensible defaults out of the box, knobs when you need them, and clocks that play nice in tests—no secret handshake required.

Deadlines ride along with you to any destination

Hand the “how much time is left?” object through your code and into other services. When two deadlines bump into each other, min picks the tighter one so nobody gets extra runway by accident.

A steady stopwatch, not wall time drama

By default we count time with a monotonic clock—the kind that keeps ticking even if the system clock jumps around after an NTP fix. Your budget does not magically grow when the wall clock does. Wall time still tags along when you want human-readable hints across machines.

Expired refuses to hide

It subclasses Exception, not StandardError, on purpose. A plain rescue => e will not swallow your timeout and pretend everything is fine.

Telemetry stays out of your way

Nothing runs until you wire something in. When you are ready, hook up Logger, ActiveSupport::Notifications, or OpenTelemetry and get spans and counts without rewriting the world.

Fake time in tests, real confidence

Skip the sleep 900 jokes. Drive a virtual clock forward, hit the edge cases, and prove your Expired branches do what you expect—fast and boring, the way tests should be.

bin/timex-lint has your back

Small static check that taps you on the shoulder when a rescue Exception sneaks inside a TIMEx.deadline block—the kind of foot-gun that makes timeouts vanish in production.

Which timeout should I grab? Tiny cheat sheet when the menu feels huge—pick the shape of your problem, then reach for the matching tool.

Waiting on a socket or a slow file read? Use TIMEx::Strategies::IO so the wait itself can time out, not just the code around it.

Churning through a normal Ruby loop? Wrap it in TIMEx.deadline and poke deadline.check! now and then so the loop can bail when the time budget is gone.

Stuck inside a C extension you cannot interrupt? TIMEx::Strategies::Subprocess or TIMEx::Composers::TwoPhase gives you a harder stop than Ruby alone can promise.

Running code you do not fully trust? Park it in TIMEx::Strategies::Subprocess; add setrlimit-style caps on your platform when you want a firmer fence around CPU or memory.

Calling a remote service that is usually fast but sometimes drags? TIMEx::Composers::Hedged fires a polite backup try—only when doing it twice cannot break things (think idempotent reads).

Everything else failed and you are out of ideas? TIMEx::Strategies::Unsafe exists, but treat it like hot sauce: tiny dash, loud code review, no surprises in prod.

Install

Let’s get TIMEx on your machine.

$ gem install timex