Exponential Backoff Patterns
Exponential Backoff: A retry strategy where the delay between attempts grows exponentially (1s, 2s, 4s, 8s...). This prevents overwhelming failing services while maximizing chances of recovery. Often combined with jitter to avoid thundering herd problems.
Basic Exponential Backoff
# Simple exponential backoff
def
exponential_backoff_retry
(max_retries:
5
, base_delay:
1
)
begin
yield
# Execute the block passed to this method
rescue
=> e
if
attempt < max_retries
delay = base_delay * (
2
** (attempt -
1
))
# 1, 2, 4, 8, 16...
puts
"Attempt #{attempt} failed: #{e.message}"
puts
"Retrying in #{delay} seconds..."
sleep(delay)
retry
else
raise
"Operation failed after #{max_retries} attempts: #{e.message}"
end
end
end
# Usage
exponential_backoff_retry(max_retries:
4
, base_delay:
0.5
)
do
api_call_that_might_fail
end
# Advanced exponential backoff with jitter
def
exponential_backoff_with_jitter
(max_retries: 5
, base_delay: 1
, max_delay: 60
)
begin
yield
rescue
=> e
if
attempt < max_retries
# Exponential backoff: 2^(attempt-1) * base_delay
exponential_delay = base_delay * (
2
** (attempt -
1
))
# Cap the delay to prevent extremely long waits
capped_delay = [exponential_delay, max_delay].min
# Add jitter to prevent thundering herd
jitter = rand(
0
..
0.1
) * capped_delay
final_delay = capped_delay + jitter
puts
"Attempt #{attempt}/#{max_retries} failed: #{e.class.name}"
puts
"Waiting #{final_delay.round(2)}s before retry..."
sleep(final_delay)
retry
else
raise
end
end
end
Configurable Retry Class
# Reusable retry class with flexible configuration
class
RetryWithBackoff
attr_reader :max_retries, :base_delay, :max_delay, :multiplier, :retryable_exceptions
def
initialize
(
retryable_exceptions: [
StandardError
]
)
@max_retries = max_retries
@base_delay = base_delay
@max_delay = max_delay
@multiplier = multiplier
@retryable_exceptions = retryable_exceptions
end
def
call
(&block)
begin
result = block.call
log_success(attempt, start_time)
return
result
rescue
=> e
if
should_retry?(e, attempt)
delay = calculate_delay(attempt)
log_retry(attempt, e, delay)
sleep(delay)
retry
else
log_failure(attempt, e, start_time)
raise
end
end
end
private
def
should_retry?
(exception, attempt)
attempt < @max_retries && retryable_exception?(exception)
end
def
retryable_exception?
(exception)
@retryable_exceptions.any? { |klass| exception.is_a?(klass) }
end
def
calculate_delay
(attempt)
exponential_delay = @base_delay * (@multiplier ** (attempt -
1
))
capped_delay = [exponential_delay, @max_delay].min
jitter = rand(
0
..
0.1
) * capped_delay
capped_delay + jitter
end
def
log_retry
(attempt, exception, delay)
puts
"[RETRY] Attempt #{attempt}/#{@max_retries} failed with #{exception.class.name}: #{exception.message}"
puts
"[RETRY] Waiting #{delay.round(2)}s before next attempt..."
end
def
log_success
(attempt, start_time)
duration = (
Time
.now - start_time).round(
2
)
puts
"[SUCCESS] Operation succeeded on attempt #{attempt} after #{duration}s"
end
def
log_failure
(attempt, exception, start_time)
duration = (
Time
.now - start_time).round(
2
)
puts
"[FAILURE] Operation failed after #{attempt} attempts and #{duration}s"
puts
"[FAILURE] Final error: #{exception.class.name} - #{exception.message}"
end
end
# Usage examples
retrier =
RetryWithBackoff
.new(
retryable_exceptions: [
Net::TimeoutError
,
Errno::ECONNREFUSED
]
)
make_network_request
end
Circuit Breaker Pattern
Circuit Breaker: A protective pattern that stops calling a failing service after a threshold of failures, then periodically tests if the service has recovered. Prevents cascade failures and gives failing services time to recover.
Circuit Breaker Implementation
# Circuit breaker states: :closed, :open, :half_open
class
CircuitBreaker
attr_reader :state, :failure_count, :last_failure_time, :success_count
def
initialize
(
failure_threshold:
5
,
# Failures before opening
recovery_timeout:
60
,
# Seconds before trying half-open
success_threshold:
3
,
# Successes needed to close
timeout:
10
# Operation timeout
)
@failure_threshold = failure_threshold
@recovery_timeout = recovery_timeout
@success_threshold = success_threshold
@timeout = timeout
end
def
call
(&block)
case
@state
when
:open
check_recovery_timeout
raise
CircuitBreakerOpenError
, "Circuit breaker is open"
if
@state == :open
when
:half_open
execute_with_monitoring(&block)
when
:closed
execute_with_monitoring(&block)
end
end
def
reset
log_state_change(
"reset"
)
end
private
def
execute_with_monitoring
(&block)
begin
result = execute_with_timeout(&block)
record_success
return
result
rescue
=> e
record_failure
raise
e
end
end
def
execute_with_timeout
(&block)
require
'timeout'
Timeout
::timeout(@timeout, &block)
rescue
Timeout::Error
raise
"Operation timed out after #{@timeout} seconds"
end
def
record_success
case
@state
when
:half_open
if
@success_count >= @success_threshold
log_state_change(
"closed - service recovered"
)
end
when
:closed
# Reset failure count on success in closed state
end
end
def
record_failure
@last_failure_time =
Time
.now
if
@state == :half_open
# Failed during half-open, go back to open
log_state_change(
"open - failed during recovery"
)
elsif
@state == :closed
&& @failure_count >= @failure_threshold
# Too many failures, open the circuit
log_state_change(
"open - threshold exceeded"
)
end
end
def
check_recovery_timeout
if
@last_failure_time && (Time
.now - @last_failure_time) > @recovery_timeout
log_state_change(
"half-open - testing recovery"
)
end
end
def
log_state_change
(reason)
puts
"[CIRCUIT BREAKER] State: #{@state} (#{reason})"
puts
"[CIRCUIT BREAKER] Failures: #{@failure_count}, Successes: #{@success_count}"
end
end
class
CircuitBreakerOpenError
< StandardError
; end
Using Circuit Breaker with Retry
# Combining circuit breaker with retry for maximum resilience
class
ResilientService
def
initialize
@circuit_breaker =
CircuitBreaker
.new(
)
@retrier =
RetryWithBackoff
.new(
retryable_exceptions: [
Net::TimeoutError
,
Errno::ECONNREFUSED
]
)
end
def
call_external_service
(data)
# Circuit breaker protects against repeated failures
# Retry handles transient failures
make_actual_service_call(data)
end
end
rescue
CircuitBreakerOpenError
=> e
puts
"Service unavailable: #{e.message}"
return
fallback_response(data)
end
def
reset_circuit_breaker
@circuit_breaker.reset
end
private
def
make_actual_service_call
(data)
# Simulate service call that might fail
if
rand < 0.3
# 30% failure rate
raise
Net::TimeoutError
, "Service timeout"
end
{ result:
"Success"
, data: data, timestamp:
Time
.now }
end
def
fallback_response
(data)
{ result:
"Fallback"
, data: data, timestamp:
Time
.now }
end
end
# Usage
service =
ResilientService
.new
# Multiple calls will trigger circuit breaker after failures
10
.times do
|i|
puts
"\n--- Call #{i + 1} ---"
result = service.call_external_service(
"data #{i}"
)
puts
"Result: #{result[:result]}"
sleep(
2
)
# Wait between calls
end
Advanced Retry Patterns
Deadline-Based Retry
# Retry with deadline instead of fixed attempt count
def
retry_with_deadline
(deadline_seconds:, base_delay:
1
)
deadline =
Time
.now + deadline_seconds
begin
yield
rescue
=> e
if
Time
.now < deadline
delay = [base_delay * (
2
** (attempt -
1
)), deadline -
Time
.now].min
if
delay >
0
puts
"Attempt #{attempt} failed, retrying in #{delay.round(2)}s..."
sleep(delay)
retry
end
end
raise
"Operation failed after #{attempt} attempts within #{deadline_seconds}s deadline"
end
end
# Usage
retry_with_deadline(deadline_seconds:
30
)
do
slow_external_service_call
end
Adaptive Retry with Success Rate
# Retry that adapts based on recent success rate
class
AdaptiveRetry
def
initialize
(window_size:
100
)
@window_size = window_size
@recent_results = []
# true for success, false for failure
end
def
call
(&block)
max_retries = calculate_max_retries
begin
result = block.call
return
result
rescue
=> e
if
attempt < max_retries
delay = calculate_adaptive_delay(attempt)
puts
"Adaptive retry #{attempt}/#{max_retries} in #{delay.round(2)}s (success rate: #{success_rate.round(2)})"
sleep(delay)
retry
else
raise
end
end
end
private
def
calculate_max_retries
rate = success_rate
case
rate
when
0.8
..1.0
then
2
# High success, few retries needed
when
0.5
..0.8
then
4
# Medium success, moderate retries
when
0.2
..0.5
then
6
# Low success, more retries
else
3
# Very low or no data, conservative
end
end
def
calculate_adaptive_delay
(attempt)
base_delay = success_rate >
0.7
?
0.5
:
2
# Shorter delays if success is likely
exponential_delay = base_delay * (
2
** (attempt -
1
))
jitter = rand(
0
..
0.1
) * exponential_delay
exponential_delay + jitter
end
def
success_rate
return
0.5
if
@recent_results.empty? # Assume 50% if no data
@recent_results.count(
true
).to_f / @recent_results.size
end
def
record_result
(success)
@recent_results << success
@recent_results.shift
if
@recent_results.size > @window_size
end
end
# Usage
adaptive_retrier =
AdaptiveRetry
.new
10
.times do
|i|
begin
result = adaptive_retrier.call
do
# Simulate varying success rates over time
raise
"Failed"
if
rand < (0.7
- i * 0.05
) # Increasing failure rate
end
puts
"Call #{i}: #{result}"
rescue
=> e
puts
"Call #{i}: Failed after all retries"
end
end