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.