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