Back to the library
Retry with exponential backoff
Variant: Exponential
A client retries a failing server and the wait doubles: 0.6s, 1.2s, 2.4s, laid on a real time axis so the doubling is geometric on screen rather than implied. The server's load meter drains at a fixed rate and each arriving request puts it straight back — only the third gap is long enough to reach the floor, which is why the fourth attempt is the one that lands.
diagraminfrastructure
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Retry with exponential backoff</title>
<style>
:root {
--ivory: #faf9f5;
--slate: #141413;
--clay: #d97757;
--oat: #e3dacc;
--rust: #b04a3f;
--gray-500: #87867f;
--loop: 5.6s;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.diagram {
width: 260px;
height: 112px;
}
.node {
fill: none;
stroke: var(--oat);
stroke-width: 1.5;
}
.link,
.axis {
stroke: var(--oat);
stroke-width: 1.5;
stroke-linecap: round;
}
.cap {
font-size: 8.5px;
letter-spacing: 0.08em;
fill: var(--gray-500);
text-anchor: middle;
}
.meter-track {
fill: var(--oat);
}
/* The axis is a real time axis: 40 user units per second. Attempts sit at
x = 20, 44, 92, 188 — gaps of 24, 48, 96 units, so 0.6s, 1.2s, 2.4s.
The doubling has to be geometric on screen or the diagram is a lie. */
.progress {
stroke: var(--clay);
stroke-width: 2.5;
stroke-dasharray: 196;
stroke-dashoffset: 196;
animation: sweep var(--loop) linear infinite;
}
.tick {
transform-box: fill-box;
transform-origin: bottom center;
opacity: 0;
}
.tick.fail {
fill: var(--rust);
}
.tick.ok {
fill: var(--clay);
}
.t1 {
animation: t1 var(--loop) linear infinite;
}
.t2 {
animation: t2 var(--loop) linear infinite;
}
.t3 {
animation: t3 var(--loop) linear infinite;
}
.t4 {
animation: t4 var(--loop) linear infinite;
}
.req {
fill: var(--clay);
opacity: 0;
transform-box: fill-box;
transform-origin: center;
animation: req var(--loop) linear infinite;
}
.rej {
fill: var(--rust);
opacity: 0;
animation: rej var(--loop) linear infinite;
}
/* Load falls 0.45/s and every arriving request adds 0.45 back. Only the
third gap is long enough to reach the floor — that is precisely why the
fourth attempt is the one that lands. */
.load {
fill: var(--rust);
transform-box: fill-box;
transform-origin: left center;
animation: load var(--loop) linear infinite;
}
.down {
fill: none;
stroke: var(--rust);
stroke-width: 1.5;
animation: down var(--loop) linear infinite;
}
.ok-ring {
fill: none;
stroke: var(--clay);
stroke-width: 2;
opacity: 0;
transform-box: fill-box;
transform-origin: center;
animation: ok-ring var(--loop) linear infinite;
}
.live {
animation: live var(--loop) linear infinite;
}
@keyframes live {
0% {
opacity: 0;
}
1.43%,
94.64% {
opacity: 1;
}
99.11%,
100% {
opacity: 0;
}
}
@keyframes sweep {
0% {
stroke-dashoffset: 196;
}
87.5%,
100% {
stroke-dashoffset: 0;
}
}
/* Flights are linear — a packet in transit is constant motion. Everything
that is a state change (a tick sprouting, load jumping, the ack ring)
runs 160-180ms on a strong ease-out. */
@keyframes t1 {
0%,
2.68% {
opacity: 0;
transform: scaleY(0.72);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
5.54%,
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes t2 {
0%,
13.39% {
opacity: 0;
transform: scaleY(0.72);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
16.25%,
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes t3 {
0%,
34.82% {
opacity: 0;
transform: scaleY(0.72);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
37.68%,
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes t4 {
0%,
77.68% {
opacity: 0;
transform: scaleY(0.72);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
80.54%,
100% {
opacity: 1;
transform: scaleY(1);
}
}
@keyframes req {
0%,
2.68% {
transform: translateX(0);
opacity: 0;
}
3.1% {
opacity: 1;
}
6.3% {
opacity: 1;
}
6.61% {
transform: translateX(120px);
opacity: 0;
}
6.62%,
13.39% {
transform: translateX(0);
opacity: 0;
}
13.81% {
opacity: 1;
}
17.1% {
opacity: 1;
}
17.32% {
transform: translateX(120px);
opacity: 0;
}
17.33%,
34.82% {
transform: translateX(0);
opacity: 0;
}
35.24% {
opacity: 1;
}
38.53% {
opacity: 1;
}
38.75% {
transform: translateX(120px);
opacity: 0;
}
38.76%,
77.68% {
transform: translateX(0);
opacity: 0;
}
78.1% {
opacity: 1;
}
81.61% {
transform: translateX(120px) scale(1);
opacity: 1;
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
84.29% {
transform: translateX(120px) scale(0.9);
opacity: 0;
}
84.3%,
100% {
transform: translateX(0) scale(1);
opacity: 0;
}
}
@keyframes rej {
0%,
6.61% {
transform: translateX(120px);
opacity: 0;
}
7.03% {
opacity: 1;
}
10.12% {
opacity: 1;
}
10.54% {
transform: translateX(0);
opacity: 0;
}
10.55%,
17.32% {
transform: translateX(120px);
opacity: 0;
}
17.74% {
opacity: 1;
}
20.83% {
opacity: 1;
}
21.25% {
transform: translateX(0);
opacity: 0;
}
21.26%,
38.75% {
transform: translateX(120px);
opacity: 0;
}
39.17% {
opacity: 1;
}
42.26% {
opacity: 1;
}
42.68% {
transform: translateX(0);
opacity: 0;
}
42.69%,
100% {
transform: translateX(120px);
opacity: 0;
}
}
@keyframes load {
0% {
transform: scaleX(0.9);
}
6.61% {
transform: scaleX(0.73);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
9.11% {
transform: scaleX(0.9);
}
17.32% {
transform: scaleX(0.69);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
19.82% {
transform: scaleX(0.9);
}
38.75% {
transform: scaleX(0.42);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
41.25% {
transform: scaleX(0.87);
}
75.7%,
99.11% {
transform: scaleX(0.02);
}
100% {
transform: scaleX(0.9);
}
}
@keyframes down {
0%,
67.86% {
opacity: 1;
}
71.07%,
99.11% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes ok-ring {
0%,
81.61% {
opacity: 0;
transform: scale(0.94);
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
84.82%,
100% {
opacity: 1;
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.live,
.progress,
.tick,
.req,
.rej,
.load,
.down,
.ok-ring {
animation: none;
}
/* Reduced motion keeps the lesson — the accumulated ticks are the whole
point and they are static anyway. Only the resolved state breathes. */
.progress {
stroke-dashoffset: 0;
}
.tick {
opacity: 1;
transform: none;
}
.req,
.rej,
.down {
opacity: 0;
}
.load {
transform: scaleX(0.02);
}
.ok-ring {
opacity: 1;
transform: none;
animation: settle 2.6s ease-in-out infinite;
}
@keyframes settle {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
}
</style>
</head>
<body>
<svg
class="diagram"
viewBox="0 0 260 112"
role="img"
aria-label="A client retries a failing server with exponential backoff: the wait doubles after each failure and the fourth attempt succeeds once load has drained"
>
<g class="structure">
<line class="link" x1="70" y1="29" x2="190" y2="29" />
<rect class="node" x="14" y="12" width="56" height="34" rx="7" />
<rect class="node" x="190" y="12" width="56" height="34" rx="7" />
<rect class="meter-track" x="198" y="36.5" width="40" height="3.5" rx="1.75" />
<line class="axis" x1="14" y1="100" x2="246" y2="100" />
</g>
<g class="live">
<rect class="load" x="198" y="36.5" width="40" height="3.5" rx="1.75" />
<rect class="down" x="190" y="12" width="56" height="34" rx="7" />
<rect class="ok-ring" x="190" y="12" width="56" height="34" rx="7" />
<line class="progress" x1="14" y1="100" x2="210" y2="100" />
<rect class="tick fail t1" x="18.75" y="86" width="2.5" height="14" />
<rect class="tick fail t2" x="42.75" y="86" width="2.5" height="14" />
<rect class="tick fail t3" x="90.75" y="86" width="2.5" height="14" />
<rect class="tick ok t4" x="186.75" y="82" width="2.5" height="18" />
<circle class="req" cx="70" cy="29" r="4.5" />
<circle class="rej" cx="70" cy="29" r="4.5" />
</g>
<g class="labels">
<text class="cap" x="42" y="29">CLIENT</text>
<text class="cap" x="218" y="29">SERVER</text>
</g>
</svg>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.