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 {
        --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,
      .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(--loop) linear infinite;
      }

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

      .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;
        animation: req var(--loop) linear infinite;
      }

      .rej {
        fill: var(--rust);
        opacity: 0;
        animation: rej var(--loop) 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(--loop) linear infinite;
      }
      .p2 {
        animation: p2 var(--loop) linear infinite;
      }
      .p3 {
        animation: p3 var(--loop) linear infinite;
      }
      .p4 {
        animation: p4 var(--loop) linear infinite;
      }

      .giveup {
        fill: none;
        stroke: var(--rust);
        stroke-width: 1.5;
        opacity: 0;
        transform-box: fill-box;
        transform-origin: center;
        animation: giveup 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: 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 2.6s 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.