Retry Pattern
Repeat a call after a transient failure under an explicit attempt limit and waiting policy.
· Last updated: 2026-10-26 · 23 min read
Overview
Suppose an order screen asks an inventory service for the remaining quantity. During deployment or a brief traffic surge, the first call may fail with 503 Service Unavailable and the same request, sent shortly afterward, succeed with 200 OK. Showing the first failure immediately turns a brief disruption into a failed order. Conversely, a loop that calls again immediately until it succeeds adds requests to an already busy service. It also never stops for a request whose outcome will stay the same, such as an invalid product code returning 400 Bad Request.
The Retry pattern repeats a failed call under defined rules. Microsoft's Azure Architecture Center describes it as handling transient failures when connecting to services or network resources by retrying the failed operation.1 These rules determine which failures to retry, how many attempts to allow, and how long to wait between them. This article collects the three in a retry policy(RetryPolicy) value.
A failure that may succeed when repeated is a transient failure: a brief network interruption, temporary service unavailability, or a timeout from a busy service. Invalid input or missing permission usually produces the same failure when repeated; this article calls that a permanent failure. The policy's RetryOn function takes a failure kind and decides whether to retry. The examples classify 503 as transient and 400 as permanent.
MaxAttempts includes the first call in its attempt limit. Without that limit, a caller may never finish during a prolonged outage. Backoff returns the wait following the nth failure. Multiplying the wait by a fixed factor after each failure is exponential backoff; a cap keeps it from growing indefinitely. Jitter adds randomness to the delay, spreading retry times so clients that failed together do not all return together.
This article calls the component that repeats work under the policy a retry executor(Retrier). It returns a record of each attempt's outcome and decision, retry or stop, so the caller can inspect the final value and why execution stopped on a particular attempt. Waiting is injected as a delay function. Production code can supply a timer; tests can supply a function that only records the requested duration.
Structure
Retrier knows nothing about what the operation does. On failure, it consults the policy's RetryOn and asks Backoff how long to wait. It delegates that wait to delay before invoking the same operation again.
History
Retrying a failed transmission after a random interval, with longer intervals after repeated failures, already appears in Metcalfe and Boggs's 1976 Ethernet paper. Each colliding station's controller generates its own random retransmission interval to avoid another collision. A new packet starts with a mean interval of one slot. After each collision, the controller waits for a random interval whose mean is twice the previous one. The authors describe this as approximating their Binary Exponential Backoff algorithm.2
HTTP defines retry conditions at the protocol level. RFC 9110 calls a method idempotent when multiple identical requests have the same intended server effect as one request. PUT, DELETE, and the safe methods GET, HEAD, OPTIONS and TRACE have this property. An idempotent request can be automatically repeated if communication fails before the response is read. A client SHOULD NOT automatically retry a non-idempotent request unless it knows the operation is actually idempotent or can detect that the original request was never applied. It also SHOULD NOT automatically retry a failed automatic retry.3 That rule concerns automatic retries performed by an HTTP client itself. Here the caller explicitly chooses the operations and attempt limits, and restricts retries to idempotent work. A server can provide an HTTP date or a number of seconds in Retry-After to indicate how long to wait. With a 503 response, it indicates how long the service is expected to remain unavailable.4 A 429 Too Many Requests response can also include Retry-After.5
In a 2015 AWS Architecture Blog article, Marc Brooker simulated clients contending for one resource. Capped exponential backoff alone still left calls clustered together. Adding jitter reduced the number of calls by more than half in the case with 100 contending clients. He argued that backoff with jitter should be the standard approach for remote clients.6 AWS's new SDK retry behavior, announced in May 2026, uses full jitter: a uniform random value between 0 and 1 multiplied by min(20,000 ms, base delay × 2^retry). The announcement says this behavior becomes the default in November 2026. Until then it is enabled with AWS_NEW_RETRIES_2026=true.7
The Google SRE book's chapter on cascading failures explains how retries can amplify an outage. It warns that retries at several layers can multiply the attempts generated by one request. It recommends limiting retries per request, setting a server-wide retry budget, and returning a separate status under overload so clients know not to retry.8 A 2019 Amazon Builders' Library article gives an example where independent retries across five layers multiply database load by 243, and recommends retrying at a single point in the stack. It also says AWS added token-bucket-based retry limiting to the AWS SDK in 2016.9
Microsoft's Azure Architecture Center documents Retry as a cloud design pattern and distinguishes cancelling, retrying immediately, and retrying after a delay according to the nature of the failure.10
Why use it?
Retries can absorb transient failures before they reach the caller. Representing the policy as a value lets each dependency use a different one: an interactive screen can make a few attempts with short waits before reporting failure, while a batch job allows more attempts and exponentially longer waits.11 Injecting the delay function makes the attempt sequence and requested waits testable without actually waiting.
.NET applications often use Polly's retry strategy rather than implementing one. ShouldHandle selects the outcomes or exceptions to retry, MaxRetryAttempts counts retries excluding the first call, and enabling UseJitter randomizes the waits.12 HttpClient has the Microsoft.Extensions.Http.Resilience package, built on Polly. Its standard handler allows up to three retries with exponential backoff and jitter, handling status codes 500 and above, 408 and 429.13 The retry option ShouldRetryAfterHeader defaults to true. In that mode, the delay follows the Retry-After header rules.14
Retries help only with transient failures. During prolonged failures they delay responses and hold resources, and repeating a business-logic error does not fix it. A service that is frequently busy needs more capacity rather than more retries.15
A retry adds work to an already struggling server. The Builders' Library calls retries selfish: the retrying client consumes extra server time to improve its own chance of success.9 As above, retries at each layer multiply attempts. Keep retries in one layer and let lower layers return the failure reason promptly.16
The next attempt cannot start while the current one is still running. Combine retries with a Timeout pattern to bound each attempt, and also bound total retry duration. Microsoft's standard handler separates an attempt timeout of 10 seconds from a total timeout of 30 seconds that includes retries.17 During a prolonged outage, further retries only add cost. Pair them with a Circuit Breaker pattern that temporarily blocks calls after continuing failures.11
Follow the wait a server specifies through Retry-After. AWS SDKs likewise use the service's x-amz-retry-after header so it can indicate when capacity should be available. The SDK bounds that value between its calculated backoff and the backoff plus 5,000 ms.18 If the requested wait exceeds the caller's total time limit, there is no room for another attempt and the call ends in failure.
Idempotency
A retry can execute the same operation more than once. If a response is lost after the service finishes processing, the caller does not know the outcome. Resending the request may repeat work that already completed.19 This is harmless for an idempotent operation, such as a lookup, whose effect stays the same when repeated. Creating orders or taking payments can instead produce a new effect each time.
The Builders' Library says APIs with side effects are unsafe to retry unless they provide idempotency. It cites Amazon EC2's RunInstances API as an example using an explicit token.9 The extended example implements this through an idempotency key. The client creates a unique key for each logical request and reuses it across every attempt. The server stores the first result for the key and returns it for subsequent requests with that key.
By default, the standard Microsoft.Extensions.Http.Resilience handler retries all HTTP methods. Its documentation warns that retrying a POST that inserts a record can duplicate data. To turn off retries for POST, PATCH, PUT, DELETE, CONNECT, it provides DisableForUnsafeHttpMethods.20 PUT and DELETE are idempotent under RFC 9110 but are not safe methods, so they are disabled as well.
Example
Run a fake inventory service under the policy. It returns transient failures twice and succeeds on the third call. The delay function records the requested durations without waiting.
using System;
using System.Collections.Generic;
// A fake inventory service: two transient failures, then success
var calls = 0;
Outcome GetStock()
{
calls++;
return calls <= 2 ? new Outcome(null, ErrorKind.Transient) : new Outcome("stock=7", null);
}
// Represent retryable failures, attempt limits and waits as data.
var policy = new RetryPolicy(
MaxAttempts: 4,
RetryOn: kind => kind == ErrorKind.Transient,
Backoff: failedAttempt => TimeSpan.FromMilliseconds(Math.Min(1000, 100 * Math.Pow(2, failedAttempt - 1))));
// Record requested waits without actually waiting.
var waited = new List<double>();
var result = Retrier.Run(policy, GetStock, delay => waited.Add(delay.TotalMilliseconds));
foreach (var line in result.Log) Console.WriteLine(line);
Console.WriteLine($"value={result.Value}, attempts={result.Attempts}, delays=[{string.Join(", ", waited)}] ms");
enum ErrorKind { Transient, Permanent }
sealed record Outcome(string? Value, ErrorKind? Error);
sealed record RetryPolicy(int MaxAttempts, Func<ErrorKind, bool> RetryOn, Func<int, TimeSpan> Backoff);
sealed record RetryResult(string? Value, ErrorKind? Error, int Attempts, List<string> Log);
static class Retrier
{
// Record each attempt and decision; stop on success, a non-retryable failure or exhausted attempts.
public static RetryResult Run(RetryPolicy policy, Func<Outcome> operation, Action<TimeSpan> delay)
{
var log = new List<string>();
for (var attempt = 1; ; attempt++)
{
var outcome = operation();
if (outcome.Error is not { } error)
{
log.Add($"attempt {attempt}: ok");
return new RetryResult(outcome.Value, null, attempt, log);
}
if (!policy.RetryOn(error))
{
log.Add($"attempt {attempt}: {error}, not retryable");
return new RetryResult(null, error, attempt, log);
}
if (attempt >= policy.MaxAttempts)
{
log.Add($"attempt {attempt}: {error}, attempts exhausted");
return new RetryResult(null, error, attempt, log);
}
var wait = policy.Backoff(attempt);
log.Add($"attempt {attempt}: {error}, retry in {wait.TotalMilliseconds} ms");
delay(wait);
}
}
}attempt 1: Transient, retry in 100 ms
attempt 2: Transient, retry in 200 ms
attempt 3: ok
value=stock=7, attempts=3, delays=[100, 200] msAfter the first two failures, Backoff chooses 100 ms and 200 ms. The third attempt returns a value and execution stops.
Extended example
Add asynchronous work, a CancellationToken, full jitter with a fixed seed, Retry-After, and an idempotency check. The policy retries only transient failures (Transient) and throttling (Throttled). Before each attempt, the executor checks cancellation; if cancelled, it records stopped: cancelled and stops. An Operation carries a declaration of idempotency. The executor rejects a non-idempotent operation before execution when the policy allows multiple attempts. OrderServer saves the first order request, then returns a failure simulating a lost response. The cancelled while waiting case simulates cancellation by the caller during the first wait.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
// Use a fixed random seed so every run produces the same jitter waits.
const int Seed = 42;
var rng = new Random(Seed);
var policy = new RetryPolicy(
MaxAttempts: 4,
RetryOn: f => f.Kind is ErrorKind.Transient or ErrorKind.Throttled,
Backoff: failedAttempt => FullJitter(rng, failedAttempt, baseMs: 100, capMs: 2000));
// Production can pass Task.Delay(delay, ct), which throws if cancelled while waiting. Here only record the wait.
Task NoWait(TimeSpan delay, CancellationToken ct) => Task.CompletedTask;
Console.WriteLine($"jitter seed {Seed}");
await Show("transient twice", Scripted("GetStock",
Outcome.Fail(ErrorKind.Transient, "503"), Outcome.Fail(ErrorKind.Transient, "503"), Outcome.Ok("stock=7")));
await Show("permanent error", Scripted("GetStock", Outcome.Fail(ErrorKind.Permanent, "400")));
await Show("transient every time", Scripted("GetStock", Outcome.Fail(ErrorKind.Transient, "503")));
await Show("throttled with Retry-After", Scripted("GetStock",
Outcome.Fail(ErrorKind.Throttled, "429", TimeSpan.FromMilliseconds(2000)), Outcome.Ok("stock=7")));
// Simulate the caller cancelling during the first wait.
using var cts = new CancellationTokenSource();
Task CancelDuringWait(TimeSpan delay, CancellationToken ct) { cts.Cancel(); return Task.CompletedTask; }
await Show("cancelled while waiting", Scripted("GetStock", Outcome.Fail(ErrorKind.Transient, "503")), CancelDuringWait, cts.Token);
var server = new OrderServer();
await Show("order without key", new Operation("CreateOrder", Idempotent: false, _ => Task.FromResult(server.Create(null))));
await Show("order with key k-1", new Operation("CreateOrder", Idempotent: true, _ => Task.FromResult(server.Create("k-1"))));
Console.WriteLine($"orders stored: {server.Stored}");
async Task Show(string title, Operation operation,
Func<TimeSpan, CancellationToken, Task>? delay = null, CancellationToken ct = default)
{
var result = await Retrier.RunAsync(policy, operation, delay ?? NoWait, ct);
Console.WriteLine($"== {title}");
foreach (var a in result.Attempts) Console.WriteLine($" #{a.Number} {a.Summary}");
Console.WriteLine($" -> {result.Stop}{(result.Value is null ? "" : " " + result.Value)}");
}
// A fake dependency returns preset outcomes, then repeats the last. A lookup is idempotent.
static Operation Scripted(string name, params Outcome[] script)
{
var next = 0;
return new Operation(name, Idempotent: true, _ => Task.FromResult(script[Math.Min(next++, script.Length - 1)]));
}
// Full jitter: choose uniformly from 0 inclusive to min(cap, base * 2^(n-1)) exclusive.
static TimeSpan FullJitter(Random rng, int failedAttempt, int baseMs, int capMs)
{
var ceiling = Math.Min(capMs, baseMs * Math.Pow(2, failedAttempt - 1));
return TimeSpan.FromMilliseconds((int)(rng.NextDouble() * ceiling));
}
enum ErrorKind { Transient, Throttled, Permanent }
sealed record Failure(ErrorKind Kind, string Code, TimeSpan? RetryAfter);
sealed record Outcome(string? Value, Failure? Error)
{
public static Outcome Ok(string value) => new(value, null);
public static Outcome Fail(ErrorKind kind, string code, TimeSpan? retryAfter = null) => new(null, new Failure(kind, code, retryAfter));
}
sealed record Operation(string Name, bool Idempotent, Func<CancellationToken, Task<Outcome>> Run);
sealed record RetryPolicy(int MaxAttempts, Func<Failure, bool> RetryOn, Func<int, TimeSpan> Backoff);
sealed record AttemptRecord(int Number, string Summary);
sealed record RetryResult(string Stop, string? Value, Failure? Error, IReadOnlyList<AttemptRecord> Attempts);
static class Retrier
{
public static async Task<RetryResult> RunAsync(
RetryPolicy policy, Operation operation,
Func<TimeSpan, CancellationToken, Task> delay, CancellationToken ct)
{
var attempts = new List<AttemptRecord>();
// Reject non-idempotent work before execution; a repeat could duplicate its effect.
if (!operation.Idempotent && policy.MaxAttempts > 1)
return new RetryResult($"refused: {operation.Name} is not idempotent", null, null, attempts);
for (var n = 1; ; n++)
{
// Check cancellation before the next attempt; record it and stop if cancelled.
if (ct.IsCancellationRequested)
return new RetryResult("stopped: cancelled", null, null, attempts);
var outcome = await operation.Run(ct);
if (outcome.Error is not { } error)
{
attempts.Add(new AttemptRecord(n, "ok"));
return new RetryResult("succeeded", outcome.Value, null, attempts);
}
var failed = $"{error.Kind} {error.Code}";
if (!policy.RetryOn(error))
{
attempts.Add(new AttemptRecord(n, failed + ", not retryable"));
return new RetryResult("stopped: not retryable", null, error, attempts);
}
if (n >= policy.MaxAttempts)
{
attempts.Add(new AttemptRecord(n, failed + ", attempts exhausted"));
return new RetryResult("stopped: attempts exhausted", null, error, attempts);
}
// Follow the server's Retry-After delay when supplied.
var (wait, source) = error.RetryAfter is { } hint ? (hint, "Retry-After") : (policy.Backoff(n), "backoff");
attempts.Add(new AttemptRecord(n, $"{failed}, wait {wait.TotalMilliseconds} ms ({source})"));
await delay(wait, ct);
}
}
}
// Return the original order for a repeated idempotency key.
sealed class OrderServer
{
private readonly Dictionary<string, string> byKey = new();
private bool dropNextResponse = true;
public int Stored { get; private set; }
public Outcome Create(string? idempotencyKey)
{
if (idempotencyKey is not null && byKey.TryGetValue(idempotencyKey, out var existing))
return Outcome.Ok(existing);
Stored++;
var id = $"order-{Stored}";
if (idempotencyKey is not null) byKey[idempotencyKey] = id;
// Save the first request, then return failure to simulate a lost response.
if (!dropNextResponse) return Outcome.Ok(id);
dropNextResponse = false;
return Outcome.Fail(ErrorKind.Transient, "connection reset");
}
}jitter seed 42
== transient twice
#1 Transient 503, wait 66 ms (backoff)
#2 Transient 503, wait 28 ms (backoff)
#3 ok
-> succeeded stock=7
== permanent error
#1 Permanent 400, not retryable
-> stopped: not retryable
== transient every time
#1 Transient 503, wait 12 ms (backoff)
#2 Transient 503, wait 104 ms (backoff)
#3 Transient 503, wait 67 ms (backoff)
#4 Transient 503, attempts exhausted
-> stopped: attempts exhausted
== throttled with Retry-After
#1 Throttled 429, wait 2000 ms (Retry-After)
#2 ok
-> succeeded stock=7
== cancelled while waiting
#1 Transient 503, wait 26 ms (backoff)
-> stopped: cancelled
== order without key
-> refused: CreateOrder is not idempotent
== order with key k-1
#1 Transient connection reset, wait 72 ms (backoff)
#2 ok
-> succeeded order-1
orders stored: 1Full jitter chooses between zero and the cap, so the second wait, 28 ms, can be shorter than the first, 66 ms. The fixed seed makes the output reproducible. Production code should not fix the seed: several instances using the same seed would synchronize their retries again. If several threads share a policy, avoid sharing a seeded Random instance and use the thread-safe Random.Shared instead.21 The order without a key never runs. The order with key k-1 is saved only once despite being resent after the lost response (orders stored: 1). OrderServer has a minimal key store. It has no atomic insert to prevent concurrent requests for one key, processing state, key retention period, or rejection of a reused key with a different payload. Cancellation is checked only between attempts. A production Task.Delay(delay, ct) throws when cancelled during a wait, so that exception must also be translated into a cancellation result. This executor has no attempt timeout, total duration limit, retry budget or circuit breaker. It does not cap Retry-After either, waits as long as the server requests, and trusts the caller's idempotency declaration without validating it.
TypeScript example
Translate the same policy and executor into TypeScript. Because Math.random does not accept a seed, the example provides a small seeded generator, mulberry32. Cancellation arrives through an AbortSignal; the executor checks signal.aborted before each attempt.
type ErrorKind = "Transient" | "Throttled" | "Permanent";
type Failure = { kind: ErrorKind; code: string; retryAfterMs?: number };
type Outcome = { ok: true; value: string } | { ok: false; error: Failure };
type Operation = { name: string; idempotent: boolean; run: (signal: AbortSignal) => Promise<Outcome> };
type RetryPolicy = {
maxAttempts: number;
retryOn: (failure: Failure) => boolean;
backoffMs: (failedAttempt: number) => number;
};
type RetryResult = { stop: string; value?: string; attempts: string[] };
const ok = (value: string): Outcome => ({ ok: true, value });
const fail = (kind: ErrorKind, code: string, retryAfterMs?: number): Outcome =>
({ ok: false, error: { kind, code, retryAfterMs } });
// A small seeded generator, mulberry32. Math.random does not accept a seed.
function seeded(seed: number): () => number {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Full jitter: choose uniformly from 0 inclusive to min(cap, base * 2^(n-1)) exclusive.
function fullJitter(random: () => number, failedAttempt: number, baseMs: number, capMs: number): number {
return Math.floor(random() * Math.min(capMs, baseMs * 2 ** (failedAttempt - 1)));
}
async function runWithRetry(
policy: RetryPolicy, op: Operation,
delay: (ms: number, signal: AbortSignal) => Promise<void>, signal: AbortSignal,
): Promise<RetryResult> {
const attempts: string[] = [];
// Reject non-idempotent work before executing it.
if (!op.idempotent && policy.maxAttempts > 1) return { stop: `refused: ${op.name} is not idempotent`, attempts };
for (let n = 1; ; n++) {
// Check cancellation before the next attempt; record it and stop if cancelled.
if (signal.aborted) return { stop: "stopped: cancelled", attempts };
const outcome = await op.run(signal);
if (outcome.ok) {
attempts.push(`#${n} ok`);
return { stop: "succeeded", value: outcome.value, attempts };
}
const failed = `#${n} ${outcome.error.kind} ${outcome.error.code}`;
if (!policy.retryOn(outcome.error)) {
attempts.push(`${failed}, not retryable`);
return { stop: "stopped: not retryable", attempts };
}
if (n >= policy.maxAttempts) {
attempts.push(`${failed}, attempts exhausted`);
return { stop: "stopped: attempts exhausted", attempts };
}
// Follow the server's Retry-After delay when supplied.
const hint = outcome.error.retryAfterMs;
const [waitMs, source] = hint !== undefined ? [hint, "Retry-After"] : [policy.backoffMs(n), "backoff"];
attempts.push(`${failed}, wait ${waitMs} ms (${source})`);
await delay(waitMs, signal);
}
}
// A fake dependency returns preset outcomes, then repeats the last. A lookup is idempotent.
function scripted(name: string, ...script: Outcome[]): Operation {
let next = 0;
return { name, idempotent: true, run: async () => script[Math.min(next++, script.length - 1)] };
}
const seed = 42;
const random = seeded(seed);
const policy: RetryPolicy = {
maxAttempts: 4,
retryOn: (f) => f.kind === "Transient" || f.kind === "Throttled",
backoffMs: (n) => fullJitter(random, n, 100, 2000),
};
// Production should pass a real timer that observes signal. This example does not wait.
const noWait = async (_ms: number, _signal: AbortSignal): Promise<void> => {};
async function show(
title: string, op: Operation,
delay: (ms: number, signal: AbortSignal) => Promise<void> = noWait, signal = new AbortController().signal,
): Promise<void> {
const result = await runWithRetry(policy, op, delay, signal);
console.log(`== ${title}`);
for (const line of result.attempts) console.log(` ${line}`);
console.log(` -> ${result.stop}${result.value === undefined ? "" : " " + result.value}`);
}
async function main(): Promise<void> {
console.log(`jitter seed ${seed}`);
await show("transient twice", scripted("GetStock", fail("Transient", "503"), fail("Transient", "503"), ok("stock=7")));
await show("permanent error", scripted("GetStock", fail("Permanent", "400")));
await show("transient every time", scripted("GetStock", fail("Transient", "503")));
await show("throttled with Retry-After", scripted("GetStock", fail("Throttled", "429", 2000), ok("stock=7")));
// Simulate the caller cancelling during the first wait.
const controller = new AbortController();
const cancelDuringWait = async (_ms: number, _signal: AbortSignal): Promise<void> => controller.abort();
await show("cancelled while waiting", scripted("GetStock", fail("Transient", "503")), cancelDuringWait, controller.signal);
await show("order without key", { name: "CreateOrder", idempotent: false, run: async () => ok("order-1") });
}
void main();jitter seed 42
== transient twice
#1 Transient 503, wait 60 ms (backoff)
#2 Transient 503, wait 89 ms (backoff)
#3 ok
-> succeeded stock=7
== permanent error
#1 Permanent 400, not retryable
-> stopped: not retryable
== transient every time
#1 Transient 503, wait 85 ms (backoff)
#2 Transient 503, wait 133 ms (backoff)
#3 Transient 503, wait 69 ms (backoff)
#4 Transient 503, attempts exhausted
-> stopped: attempts exhausted
== throttled with Retry-After
#1 Throttled 429, wait 2000 ms (Retry-After)
#2 ok
-> succeeded stock=7
== cancelled while waiting
#1 Transient 503, wait 52 ms (backoff)
-> stopped: cancelled
== order without key
-> refused: CreateOrder is not idempotentThe different random generator produces jitter values unlike the C# example. The decision order, cancellation, success, non-retryable failure, exhausted attempts, then Retry-After, is the same, as is the rejection of non-idempotent work.
See also
Each C# example is a standalone .NET 10 console program.
Compile the TypeScript examples to JavaScript targeting ES2022 or later, then run them in Node.js.
Footnotes
- Microsoft Learn, Azure Architecture Center. Retry pattern. Introduction; Context and problem. ↩
- Robert M. Metcalfe, David R. Boggs. "Ethernet: Distributed Packet Switching for Local Computer Networks". Communications of the ACM 19(5): 395-404 (1976). DOI, article copy. Section 2, System Summary; Section 4.4, Controller. ↩
- R. Fielding, M. Nottingham, J. Reschke (eds.). RFC 9110: HTTP Semantics (2022). Sections 9.2.1, Safe Methods, and 9.2.2, Idempotent Methods. ↩
- R. Fielding, M. Nottingham, J. Reschke (eds.). RFC 9110: HTTP Semantics (2022). Sections 10.2.3, Retry-After, and 15.6.4, 503 Service Unavailable. ↩
- M. Nottingham, R. Fielding. RFC 6585: Additional HTTP Status Codes (2012). Section 4, 429 Too Many Requests. ↩
- Marc Brooker. Exponential Backoff And Jitter. AWS Architecture Blog (2015). ↩
- AWS SDKs and Tools Reference Guide. Retry behavior. Opening Important box; How long does the SDK wait. Matthew Miller. Announcing updated retry behavior for AWS SDKs and tools. AWS Developer Tools Blog (2026-05-20). ↩
- Mike Ulrich. "Addressing Cascading Failures". Chapter 22 of the Google SRE book. Copyright 2017 Google, Inc.; published by O'Reilly Media. Retries section. ↩
- Amazon Builders' Library. Timeouts, retries, and backoff with jitter (2019). Retries and backoff. ↩
- Microsoft Learn, Azure Architecture Center. Retry pattern. Retry strategies. ↩
- Microsoft Learn, Azure Architecture Center. Retry pattern. Impact on performance. ↩
- Polly documentation. Retry resilience strategy. Defaults table. ↩
- Microsoft Learn. Build resilient HTTP apps: Key development patterns. Standard resilience handler defaults; Retries and circuit breakers. ↩
- Microsoft Learn. HttpRetryStrategyOptions.ShouldRetryAfterHeader. Property Value, Remarks. ↩
- Microsoft Learn, Azure Architecture Center. Retry pattern. When to use this pattern. ↩
- Microsoft Learn, Azure Architecture Center. Retry pattern. General guidance. ↩
- Microsoft Learn. Build resilient HTTP apps: Key development patterns. Standard resilience handler defaults. ↩
- AWS SDKs and Tools Reference Guide. Retry behavior. Server-directed retry timing. ↩
- Microsoft Learn, Azure Architecture Center. Retry pattern. Idempotency. ↩
- Microsoft Learn. Build resilient HTTP apps: Key development patterns. Disable retries for a given list of HTTP methods. ↩
- Microsoft Learn. Random.Shared Property. ↩