Back to the library

Retry budget exhausted

Variant: Budget exhausted

Backoff doing everything right against an outage that outlasts it. The gaps double exactly as in the exponential variant, but the server is hard-down, so nothing the client does moves its meter. The four pips inside the client are the retry budget; each goes out as its request leaves, and when the last one does the client turns rust and the timeline stops advancing. Use it to document give-up and dead-letter behaviour.

errordiagraminfrastructure

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 budget exhausted</title>
    <style>
      :root {
        /* Neutrals — fixed, never remixed. */
        --ivory: #faf9f5;
        --paper: #ffffff;
        --oat: #e3dacc;
        --slate: #141413;

        /* Knobs — the remix API. A knob exists iff declared here and used below. */
        --hue: 38.8;
        --lift: 0;
        --chroma: 1;
        --speed: 1;

        /* Accents — derived from the knobs with the family's offsets baked in,
           so one hue roll moves them together. Defaults reproduce the original hex. */
        --clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
        --clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
        --olive: oklch(
          calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
        );
        --rust: oklch(
          calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
        );

        /* The caption gray sits between slate and oat and belongs to neither
           family, so it stays a fixed neutral rather than riding the hue. */
        --_cap: #87867f;

        /* Clock — every duration in the document runs off --t. The reduced-motion
           breath is its own period and divides by the same --speed. */
        --cycle: 5.6s;
        --t: calc(var(--cycle) / var(--speed));
        --_calm: calc(2.6s / var(--speed));
      }

      * {
        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;
      }

      /* The viewBox is 1:1 with the rendered width, so a user unit is a CSS
         pixel: 11 is the floor for this to stay legible on a phone stage. */
      .cap {
        font-size: 11px;
        letter-spacing: 0.08em;
        fill: var(--_cap);
        text-anchor: middle;
      }

      .meter-track,
      .socket {
        fill: var(--oat);
      }

      /* Identical 40 units-per-second axis and identical 24 / 48 / 96 unit gaps
         as the succeeding variant. The backoff is doing its job — the outage is
         simply longer than four attempts can cover. */
      .progress {
        stroke: var(--clay);
        stroke-width: 2.5;
        stroke-dasharray: 192;
        stroke-dashoffset: 192;
        animation: sweep var(--t) linear infinite;
      }

      .tick {
        fill: var(--rust);
        transform-box: fill-box;
        transform-origin: bottom center;
        opacity: 0;
      }

      .t1 {
        animation: t1 var(--t) linear infinite;
      }
      .t2 {
        animation: t2 var(--t) linear infinite;
      }
      .t3 {
        animation: t3 var(--t) linear infinite;
      }
      .t4 {
        animation: t4 var(--t) linear infinite;
      }

      .req {
        fill: var(--clay);
        opacity: 0;
        animation: req var(--t) linear infinite;
      }

      .rej {
        fill: var(--rust);
        opacity: 0;
        animation: rej var(--t) linear infinite;
      }

      /* The server bar is flat and full for the whole loop: this is a hard
         outage, not a load problem, so nothing the client does moves it. All
         the state that changes here lives on the client side. */
      .load {
        fill: var(--rust);
        transform-box: fill-box;
        transform-origin: left center;
        transform: scaleX(0.9);
      }

      .down {
        fill: none;
        stroke: var(--rust);
        stroke-width: 1.5;
      }

      /* One pip per remaining attempt. Each goes out at the moment its request
         leaves, so the budget is visibly spent before the give-up lands. */
      .pip {
        fill: var(--clay);
      }

      .p1 {
        animation: p1 var(--t) linear infinite;
      }
      .p2 {
        animation: p2 var(--t) linear infinite;
      }
      .p3 {
        animation: p3 var(--t) linear infinite;
      }
      .p4 {
        animation: p4 var(--t) linear infinite;
      }

      .giveup {
        fill: none;
        stroke: var(--rust);
        stroke-width: 1.5;
        opacity: 0;
        transform-box: fill-box;
        transform-origin: center;
        animation: giveup var(--t) linear infinite;
      }

      .live {
        animation: live var(--t) linear infinite;
      }

      @keyframes live {
        0% {
          opacity: 0;
        }
        1.43%,
        94.64% {
          opacity: 1;
        }
        99.11%,
        100% {
          opacity: 0;
        }
      }

      @keyframes sweep {
        0% {
          stroke-dashoffset: 192;
        }
        85.71%,
        100% {
          stroke-dashoffset: 0;
        }
      }

      @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 p1 {
        0%,
        2.68% {
          opacity: 1;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        5.18%,
        100% {
          opacity: 0;
        }
      }

      @keyframes p2 {
        0%,
        13.39% {
          opacity: 1;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        15.89%,
        100% {
          opacity: 0;
        }
      }

      @keyframes p3 {
        0%,
        34.82% {
          opacity: 1;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        37.32%,
        100% {
          opacity: 0;
        }
      }

      @keyframes p4 {
        0%,
        77.68% {
          opacity: 1;
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        80.18%,
        100% {
          opacity: 0;
        }
      }

      /* Flight is linear — constant motion, not a state change. The give-up
         frame is 180ms ease-out, landing the instant the last rejection is home. */
      @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.3% {
          opacity: 1;
        }
        81.61% {
          transform: translateX(120px);
          opacity: 0;
        }
        81.62%,
        100% {
          transform: translateX(0);
          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%,
        81.61% {
          transform: translateX(120px);
          opacity: 0;
        }
        82.03% {
          opacity: 1;
        }
        85.23% {
          opacity: 1;
        }
        85.54% {
          transform: translateX(0);
          opacity: 0;
        }
        85.55%,
        100% {
          transform: translateX(120px);
          opacity: 0;
        }
      }

      @keyframes giveup {
        0%,
        85.54% {
          opacity: 0;
          transform: scale(0.97);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        88.75%,
        100% {
          opacity: 1;
          transform: scale(1);
        }
      }

      @media (prefers-reduced-motion: reduce) {
        .live,
        .progress,
        .tick,
        .req,
        .rej,
        .pip,
        .giveup {
          animation: none;
        }

        /* Reduced motion holds the terminal state — widening ticks, an empty
           budget, a dead client. Only the give-up frame breathes. */
        .progress {
          stroke-dashoffset: 0;
        }

        .tick {
          opacity: 1;
          transform: none;
        }

        .req,
        .rej,
        .pip {
          opacity: 0;
        }

        .giveup {
          opacity: 1;
          transform: none;
          animation: dead var(--_calm) ease-in-out infinite;
        }

        @keyframes dead {
          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 hard-down server with exponential backoff, spends its budget of four attempts, and gives up"
    >
      <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="socket" x="23.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <rect class="socket" x="33.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <rect class="socket" x="43.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <rect class="socket" x="53.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <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="pip p1" x="23.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <rect class="pip p2" x="33.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <rect class="pip p3" x="43.5" y="36.5" width="7" height="3.5" rx="1.75" />
        <rect class="pip p4" x="53.5" y="36.5" width="7" height="3.5" rx="1.75" />

        <rect class="giveup" x="14" y="12" width="56" height="34" rx="7" />

        <line class="progress" x1="14" y1="100" x2="206" y2="100" />

        <rect class="tick t1" x="18.75" y="86" width="2.5" height="14" />
        <rect class="tick t2" x="42.75" y="86" width="2.5" height="14" />
        <rect class="tick t3" x="90.75" y="86" width="2.5" height="14" />
        <rect class="tick 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.