Retry A Block N Times With Exponential Backoff In Ruby
Network calls, external APIs, and flaky I/O fail intermittently. Retrying immediately usually fails again — the remote service needs a moment. Exponential backoff spaces retries out so each attempt has a better chance than the last.
Description
Wrap the risky work in a block, catch the exceptions you consider retryable, and sleep for a delay that doubles on each attempt. Add jitter — a small random offset — so that many clients retrying at once don’t all hit the service on the same tick.
Rescue a specific list of exception classes rather than StandardError. Retrying a TypeError just wastes time and hides a bug; retrying a timeout is what you want.
Re-raise on the final attempt so the caller sees the real failure rather than a nil.
Sample input:
with_retries(attempts: 4, base: 0.5) { Faraday.get("https://api.example.com/orders") }
Sample Output:
# attempt 1 fails -> sleeps ~0.5s
# attempt 2 fails -> sleeps ~1.0s
# attempt 3 fails -> sleeps ~2.0s
# attempt 4 raises Faraday::TimeoutError to the caller
Answer
RETRYABLE = [Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout].freeze
def with_retries(attempts: 3, base: 0.5, max: 30.0, on: RETRYABLE)
tries = 0
begin
tries += 1
yield(tries)
rescue *on => e
raise if tries >= attempts
delay = [base * (2**(tries - 1)), max].min
delay += delay * rand * 0.25 # jitter, up to +25%
warn "retry #{tries}/#{attempts} after #{e.class}: sleeping #{delay.round(2)}s"
sleep delay
retry
end
end
# Usage
response = with_retries(attempts: 4, base: 0.5) do |attempt|
Faraday.get("https://api.example.com/orders")
end
# Retry only on a condition rather than an exception
def with_retries_until(attempts: 3, base: 0.5)
attempts.times do |i|
result = yield
return result if result
sleep [base * (2**i), 30.0].min
end
nil
end
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀