Ruby Logo

Retry-patterns

Learn about retry-patterns in Ruby programming.

Home Ruby Retry-patterns

Ruby Retry Patterns Mastery

Master Ruby's retry mechanisms and advanced retry patterns. Learn how to build resilient applications that gracefully handle transient failures with exponential backoff, circuit breakers, and sophisticated retry strategies.

Understanding Retry Patterns

Retry Patterns: Strategies for automatically retrying failed operations with intelligent delays and failure handling. Essential for building resilient applications that can handle transient network issues, temporary service outages, and other recoverable failures.

Benefits Why Use Retry Patterns?

  • Fault tolerance: Handle temporary failures gracefully
  • User experience: Reduce failed operations seen by users
  • Service reliability: Increase overall system uptime
  • Cost efficiency: Recover from failures without human intervention
  • Network resilience: Handle intermittent connectivity issues
  • Load management: Prevent cascade failures with backoff

Types Common Failure Scenarios

  • Network timeouts: Slow or interrupted connections
  • Service overload: Temporary capacity limitations
  • Database deadlocks: Transaction conflicts
  • Rate limiting: API throttling and quotas
  • Resource contention: File locks, memory pressure
  • Deployment issues: Rolling updates and restarts

Real-World Analogy

Think of retry patterns like a persistent salesperson: If a customer doesn't answer the door immediately, a good salesperson doesn't give up. They wait a moment, then try again. If still no answer, they wait a bit longer before the next attempt. After several tries with increasing delays, they might leave a note and try again later. This gradual, intelligent approach maximizes success while avoiding being annoying.

Basic Retry with Ruby's `retry` Keyword

Ruby's retry: The `retry` keyword allows you to restart the begin block when an exception occurs. It's the foundation for building more sophisticated retry patterns.

Simple Retry Examples

# Basic retry - dangerous infinite loop!
begin
risky_operation
rescue
SomeError
retry
# Will retry forever - DON'T DO THIS!
end

# Safe retry with attempt counter
max_retries =
3
attempt =
0

begin
attempt +=
1
puts
"Attempt #{attempt}"
risky_operation

rescue
StandardError
=> e
if
attempt < max_retries
puts
"Failed: #{e.message}. Retrying..."
retry
else
puts
"Max retries exceeded. Giving up."
raise
end
end

# Retry with sleep to avoid hammering
max_retries =
5
attempt =
0

begin
attempt +=
1
fetch_data_from_api

rescue
Net::TimeoutError
,
Net::HTTPError
=> e
if
attempt < max_retries
delay = attempt *
2
# Linear backoff
puts
"Network error: #{e.message}. Retrying in #{delay}s..."
sleep(delay)
retry
else
raise
"Failed after #{max_retries} attempts: #{e.message}"
end
end

Selective Retry Based on Exception Type

# Only retry specific, recoverable errors
def
reliable_api_call
(url)
max_retries =
3
attempt =
0

begin
attempt +=
1
response =
HTTP
.timeout(
10
).get(url)

# Don't retry on successful responses
return
response
if
response.status.success?

# Handle specific HTTP errors
case
response.status.code
when
429
# Rate limited
raise
"Rate limited"
when
500
..
599
# Server errors
raise
"Server error: #{response.status}"
else
raise
"HTTP error: #{response.status}"
end

# Retry on specific errors only
rescue
Net::TimeoutError
,
Net::ReadTimeout
=> e
if
attempt < max_retries
puts
"Timeout (attempt #{attempt}): #{e.message}"
sleep(
2
** attempt)
# Exponential backoff
retry
else
raise
end

rescue
StandardError
=> e
if
e.message.include?(
"Rate limited"
) && attempt < max_retries
delay =
60
+ rand(
30
)
# Wait longer for rate limits
puts
"Rate limited. Waiting #{delay}s before retry..."
sleep(delay)
retry
elsif
e.message.include?(
"Server error"
) && attempt < max_retries
puts
"Server error (attempt #{attempt}): #{e.message}"
sleep(
3
** attempt)
# Exponential backoff
retry
else
# Don't retry client errors (4xx) or unknown errors
raise
end
end
end

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
)
attempt =
0

begin
attempt +=
1
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
)
attempt =
0

begin
attempt +=
1
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
(
max_retries:
3
,
base_delay:
1
,
max_delay:
60
,
multiplier:
2
,
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)
attempt =
0
start_time =
Time
.now

begin
attempt +=
1
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(
max_retries:
5
,
base_delay:
0.5
,
retryable_exceptions: [
Net::TimeoutError
,
Errno::ECONNREFUSED
]
)

result = retrier.call
do
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

@state =
:closed
@failure_count =
0
@success_count =
0
@last_failure_time =
nil
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
@state =
:closed
@failure_count =
0
@success_count =
0
@last_failure_time =
nil
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
@success_count +=
1
if
@success_count >= @success_threshold
@state =
:closed
@failure_count =
0
@success_count =
0
log_state_change(
"closed - service recovered"
)
end
when
:closed
# Reset failure count on success in closed state
@failure_count =
0
end
end

def
record_failure
@failure_count +=
1
@last_failure_time =
Time
.now

if
@state ==
:half_open
# Failed during half-open, go back to open
@state =
:open
@success_count =
0
log_state_change(
"open - failed during recovery"
)
elsif
@state ==
:closed
&& @failure_count >= @failure_threshold
# Too many failures, open the circuit
@state =
:open
log_state_change(
"open - threshold exceeded"
)
end
end

def
check_recovery_timeout
if
@last_failure_time && (
Time
.now - @last_failure_time) > @recovery_timeout
@state =
:half_open
@success_count =
0
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(
failure_threshold:
3
,
recovery_timeout:
30
,
success_threshold:
2
)

@retrier =
RetryWithBackoff
.new(
max_retries:
3
,
base_delay:
1
,
retryable_exceptions: [
Net::TimeoutError
,
Errno::ECONNREFUSED
]
)
end

def
call_external_service
(data)
# Circuit breaker protects against repeated failures
@circuit_breaker.call
do
# Retry handles transient failures
@retrier.call
do
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
attempt =
0

begin
attempt +=
1
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
attempt =
0

begin
attempt +=
1
result = block.call
record_result(
true
)
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
record_result(
false
)
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
"Success #{i}"
end
puts
"Call #{i}: #{result}"
rescue
=> e
puts
"Call #{i}: Failed after all retries"
end
end

Interactive Practice: Retry Patterns

Practice Time: Experiment with different retry patterns and see how they handle various failure scenarios. Try adjusting parameters to understand their impact on retry behavior.

Interactive Code Runner

Ruby Code Editor
Output will appear here when you run the code...

Best Practices & Guidelines

✅ Best Practices

  • Selective retry: Only retry transient, recoverable errors
  • Exponential backoff: Use increasing delays to reduce load
  • Add jitter: Randomize delays to prevent thundering herd
  • Set max delay caps: Prevent extremely long wait times
  • Use circuit breakers: Protect against cascade failures
  • Log retry attempts: Monitor and debug retry behavior
  • Consider deadlines: Set total time limits, not just attempt counts
  • Test retry logic: Verify behavior under various failure scenarios

❌ Common Pitfalls

  • Infinite retry loops: Always set maximum attempt limits
  • Retrying non-idempotent operations: Can cause duplicate actions
  • Fixed delay retry: Can overwhelm recovering services
  • Retrying non-transient errors: Wastes resources on permanent failures
  • No circuit breakers: Can cause cascade failures
  • Synchronous retry in user requests: Poor user experience
  • Not considering downstream impact: Retry storms can make problems worse
  • Lack of monitoring: Can't optimize without observability

When to Use Each Pattern

Simple Retry:
  • Quick operations
  • Rare failures
  • Known good services
  • Testing scenarios
Exponential Backoff:
  • Network operations
  • API calls
  • Database operations
  • File system operations
Circuit Breaker:
  • External services
  • Distributed systems
  • High-traffic applications
  • Mission-critical systems

Retry Patterns Mastery Summary

You've Mastered Ruby Retry Patterns!

Basic Retry

Simple attempt counting with controlled retries

Exponential Backoff

Intelligent delays with jitter and caps

Circuit Breakers

Protective patterns preventing cascade failures

Retry patterns are essential for building resilient applications. They help you handle transient failures gracefully while protecting your systems from overload. Combine different strategies based on your specific needs, and always monitor their effectiveness in production environments.

Custom Exceptions & Error Classes
Quiz-error-handling

Quick Navigation

Read Topic
Watch Video Tutorial

Related Topics

Variables & Constants → Arrays → Methods →

Back to Ruby Home

Video Tutorial

Watch and learn retry-patterns

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!