Back to the library

Retry at a fixed interval

Variant: Fixed interval

The same client and the same failing server, retrying every 0.8s forever. It uses the identical time axis as the backoff variant, so the evenly spaced ticks are directly comparable. The load meter sheds less between attempts than each attempt puts back, so the trough never approaches the floor — the retries are what hold the server down. Show it beside the backoff version; the contrast is the argument.

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 at a fixed interval</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.2s;
        --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 {
        fill: var(--oat);
      }

      /* Same 40 user-units-per-second axis as the backoff variants, so the two
         can be read side by side. Here every gap is 32 units — 0.8s, forever. */
      .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;
      }
      .t5 {
        animation: t5 var(--t) linear infinite;
      }
      .t6 {
        animation: t6 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;
      }

      /* Load drains at the same 0.45/s as the backoff variants. A fixed 0.8s
         gap gives it 0.66s of draining before the next arrival lands, so it
         sheds 0.30 — and the arrival puts exactly 0.30 back. Every cycle is a
         wash: the trough never approaches the floor and the server is held
         down by the retries themselves. */
      .load {
        fill: var(--rust);
        transform-box: fill-box;
        transform-origin: left center;
        animation: load var(--t) linear infinite;
      }

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

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

      @keyframes live {
        0% {
          opacity: 0;
        }
        1.54%,
        93.27% {
          opacity: 1;
        }
        99.04%,
        100% {
          opacity: 0;
        }
      }

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

      /* Flight is linear — a packet in transit is constant motion. The ticks
         sprout from the axis in 160ms on a strong ease-out; that is the only
         thing here that is a state change rather than travel. */
      @keyframes t1 {
        0%,
        3.85% {
          opacity: 0;
          transform: scaleY(0.72);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        6.93%,
        100% {
          opacity: 1;
          transform: scaleY(1);
        }
      }

      @keyframes t2 {
        0%,
        19.23% {
          opacity: 0;
          transform: scaleY(0.72);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        22.31%,
        100% {
          opacity: 1;
          transform: scaleY(1);
        }
      }

      @keyframes t3 {
        0%,
        34.62% {
          opacity: 0;
          transform: scaleY(0.72);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        37.7%,
        100% {
          opacity: 1;
          transform: scaleY(1);
        }
      }

      @keyframes t4 {
        0%,
        50% {
          opacity: 0;
          transform: scaleY(0.72);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        53.08%,
        100% {
          opacity: 1;
          transform: scaleY(1);
        }
      }

      @keyframes t5 {
        0%,
        65.38% {
          opacity: 0;
          transform: scaleY(0.72);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        68.46%,
        100% {
          opacity: 1;
          transform: scaleY(1);
        }
      }

      @keyframes t6 {
        0%,
        80.77% {
          opacity: 0;
          transform: scaleY(0.72);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        83.85%,
        100% {
          opacity: 1;
          transform: scaleY(1);
        }
      }

      @keyframes req {
        0%,
        3.85% {
          transform: translateX(0);
          opacity: 0;
        }
        4.27% {
          opacity: 1;
        }
        7.77% {
          opacity: 1;
        }
        8.08% {
          transform: translateX(120px);
          opacity: 0;
        }
        8.09%,
        19.23% {
          transform: translateX(0);
          opacity: 0;
        }
        19.65% {
          opacity: 1;
        }
        23.15% {
          opacity: 1;
        }
        23.46% {
          transform: translateX(120px);
          opacity: 0;
        }
        23.47%,
        34.62% {
          transform: translateX(0);
          opacity: 0;
        }
        35.04% {
          opacity: 1;
        }
        38.54% {
          opacity: 1;
        }
        38.85% {
          transform: translateX(120px);
          opacity: 0;
        }
        38.86%,
        50% {
          transform: translateX(0);
          opacity: 0;
        }
        50.42% {
          opacity: 1;
        }
        53.92% {
          opacity: 1;
        }
        54.23% {
          transform: translateX(120px);
          opacity: 0;
        }
        54.24%,
        65.38% {
          transform: translateX(0);
          opacity: 0;
        }
        65.8% {
          opacity: 1;
        }
        69.31% {
          opacity: 1;
        }
        69.62% {
          transform: translateX(120px);
          opacity: 0;
        }
        69.63%,
        80.77% {
          transform: translateX(0);
          opacity: 0;
        }
        81.19% {
          opacity: 1;
        }
        84.69% {
          opacity: 1;
        }
        85% {
          transform: translateX(120px);
          opacity: 0;
        }
        85.01%,
        100% {
          transform: translateX(0);
          opacity: 0;
        }
      }

      @keyframes rej {
        0%,
        8.08% {
          transform: translateX(120px);
          opacity: 0;
        }
        8.5% {
          opacity: 1;
        }
        12% {
          opacity: 1;
        }
        12.31% {
          transform: translateX(0);
          opacity: 0;
        }
        12.32%,
        23.46% {
          transform: translateX(120px);
          opacity: 0;
        }
        23.88% {
          opacity: 1;
        }
        27.38% {
          opacity: 1;
        }
        27.69% {
          transform: translateX(0);
          opacity: 0;
        }
        27.7%,
        38.85% {
          transform: translateX(120px);
          opacity: 0;
        }
        39.27% {
          opacity: 1;
        }
        42.77% {
          opacity: 1;
        }
        43.08% {
          transform: translateX(0);
          opacity: 0;
        }
        43.09%,
        54.23% {
          transform: translateX(120px);
          opacity: 0;
        }
        54.65% {
          opacity: 1;
        }
        58.15% {
          opacity: 1;
        }
        58.46% {
          transform: translateX(0);
          opacity: 0;
        }
        58.47%,
        69.62% {
          transform: translateX(120px);
          opacity: 0;
        }
        70.04% {
          opacity: 1;
        }
        73.54% {
          opacity: 1;
        }
        73.85% {
          transform: translateX(0);
          opacity: 0;
        }
        73.86%,
        85% {
          transform: translateX(120px);
          opacity: 0;
        }
        85.42% {
          opacity: 1;
        }
        88.92% {
          opacity: 1;
        }
        89.23% {
          transform: translateX(0);
          opacity: 0;
        }
        89.24%,
        100% {
          transform: translateX(120px);
          opacity: 0;
        }
      }

      @keyframes load {
        0% {
          transform: scaleX(0.79);
        }
        8.08% {
          transform: scaleX(0.6);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        10.77% {
          transform: scaleX(0.9);
        }
        23.46% {
          transform: scaleX(0.6);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        26.15% {
          transform: scaleX(0.9);
        }
        38.85% {
          transform: scaleX(0.6);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        41.54% {
          transform: scaleX(0.9);
        }
        54.23% {
          transform: scaleX(0.6);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        56.92% {
          transform: scaleX(0.9);
        }
        69.62% {
          transform: scaleX(0.6);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        72.31% {
          transform: scaleX(0.9);
        }
        85% {
          transform: scaleX(0.6);
          animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
        }
        87.69% {
          transform: scaleX(0.9);
        }
        99.04% {
          transform: scaleX(0.63);
        }
        100% {
          transform: scaleX(0.79);
        }
      }

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

        /* Reduced motion keeps the lesson: evenly spaced ticks and a meter that
           never drains. Only the down state breathes. */
        .progress {
          stroke-dashoffset: 0;
        }

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

        .req,
        .rej {
          opacity: 0;
        }

        .load {
          transform: scaleX(0.9);
        }

        .down {
          animation: strain var(--_calm) ease-in-out infinite;
        }

        @keyframes strain {
          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 at a fixed interval: the gaps never grow, load never drains, and every attempt is rejected"
    >
      <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" />

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

        <rect class="tick t1" x="20.75" y="86" width="2.5" height="14" />
        <rect class="tick t2" x="52.75" y="86" width="2.5" height="14" />
        <rect class="tick t3" x="84.75" y="86" width="2.5" height="14" />
        <rect class="tick t4" x="116.75" y="86" width="2.5" height="14" />
        <rect class="tick t5" x="148.75" y="86" width="2.5" height="14" />
        <rect class="tick t6" x="180.75" y="86" width="2.5" height="14" />

        <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.