Advanced
Chapter 17 · 10 min read
Retry Logic & Resilience Testing
Implement retry mechanisms for flaky APIs, test rate limiting, handle transient failures, and build resilient test suites.
Retry Logic & Resilience
APIs can have transient failures — network hiccups, rate limits, temporary outages. A resilient test suite handles these gracefully with retry logic, backoff strategies, and proper timeout management.
Retry Strategies
- Fixed delay — Wait the same duration between retries
- Exponential backoff — Double the wait time after each retry (1s, 2s, 4s, 8s)
- Jitter — Add randomness to avoid thundering herd
Rate Limiting
APIs enforce rate limits to prevent abuse. Your tests should respect these limits and test the API's behavior when limits are exceeded (429 Too Many Requests).
retry-logic.js
// Retry wrapper with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const retryableStatuses = [429, 500, 502, 503, 504];
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!retryableStatuses.includes(response.status)) {
return response; // Success or non-retryable error
}
// Check for Retry-After header
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Attempt ${attempt} got ${response.status}, retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
lastError = new Error(`HTTP ${response.status}`);
} catch (error) {
lastError = error;
if (attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt} failed: ${error.message}, retrying...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
throw lastError;
}
// Usage
const response = await fetchWithRetry(
'https://jsonplaceholder.typicode.com/posts/1'
);
const data = await response.json();
console.assert(response.status === 200);
// Test rate limiting behavior
async function testRateLimit() {
const requests = Array(10).fill().map(() =>
fetch('https://jsonplaceholder.typicode.com/posts')
);
const responses = await Promise.all(requests);
const statuses = responses.map(r => r.status);
console.log('Rate limit test statuses:', statuses);
}
await testRateLimit();
console.log('Retry logic tests passed!');
RetryHelper.java
package com.apitesting.utils;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import java.util.Set;
import java.util.function.Supplier;
public class RetryHelper {
private static final Set<Integer> RETRYABLE = Set.of(429, 500, 502, 503, 504);
public static Response withRetry(
Supplier<Response> requestSupplier,
int maxRetries) {
Response response = null;
Exception lastException = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
response = requestSupplier.get();
if (!RETRYABLE.contains(response.getStatusCode())) {
return response;
}
long delay = (long) Math.pow(2, attempt) * 1000
+ (long)(Math.random() * 1000);
System.out.printf("Attempt %d got %d, retrying in %dms...%n",
attempt, response.getStatusCode(), delay);
Thread.sleep(delay);
} catch (Exception e) {
lastException = e;
if (attempt < maxRetries) {
try {
Thread.sleep((long) Math.pow(2, attempt) * 1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
if (response != null) return response;
throw new RuntimeException("All retries exhausted", lastException);
}
}
// Usage:
// Response resp = RetryHelper.withRetry(
// () -> given().baseUri(BASE).when().get("/posts/1"),
// 3
// );
helpers/retry.py
"""Retry logic with exponential backoff"""
import time
import random
import requests
import functools
RETRYABLE_STATUSES = {429, 500, 502, 503, 504}
def retry_request(max_retries=3, backoff_base=2):
"""Decorator for retrying API requests"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_retries + 1):
try:
response = func(*args, **kwargs)
if response.status_code not in RETRYABLE_STATUSES:
return response
# Check Retry-After header
retry_after = response.headers.get('Retry-After')
if retry_after:
delay = int(retry_after)
else:
delay = backoff_base ** attempt + random.random()
print(f"Attempt {attempt}: {response.status_code}, "
f"retrying in {delay:.1f}s...")
time.sleep(delay)
except requests.RequestException as e:
last_exception = e
if attempt < max_retries:
delay = backoff_base ** attempt
print(f"Attempt {attempt} failed: {e}, retrying...")
time.sleep(delay)
if last_exception:
raise last_exception
return response
return wrapper
return decorator
# Usage
@retry_request(max_retries=3)
def get_post(post_id):
return requests.get(
f'https://jsonplaceholder.typicode.com/posts/{post_id}'
)
# Or use directly with ApiClient:
# class ResilientApiClient(ApiClient):
# @retry_request(max_retries=3)
# def request(self, method, path, **kwargs):
# return super().request(method, path, **kwargs)
API Testing
Advanced
Retry Logic & Resilience Testing
Written by PV
© 2026 All Rights Reserved