Requests streaming into a load balancer and fanning out across four servers, each with its own request counter and connection bar.
╭───────────────────────╮
│ web-1 ● │
╭────────────●────│ served 2000 │
│ │ conns ■■□□□□□□ │
│ ╰───────────────────────╯
│
│ ╭───────────────────────╮
│ │ web-2 ● │
╔════════════╗ ├─────────────────│ served 3000 │
incoming ║ balancer ║ ● │ conns ■■□□□□□□ │
requests ║ ║ │ ╰───────────────────────╯
───●────●───────●────●║ least conn ║───────●──┼
║ ║ │ ╭───────────────────────╮
║ 556 req/s ║ │ │ web-3 ● │
╚════════════╝ ├─────●───────────│ served 4000 │
│ │ conns ■■□□□□□□ │
│ ╰───────────────────────╯
│
│ ╭───────────────────────╮
│ │ web-4 ● │
╰─────────────────│ served 5000 │
│ conns ■■■■□□□□ │
╰───────────────────────╯
● GET ● POST ● PUT ● DELETE total 14000 Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="load-balancer"></ascii-art>// copy load-balancer.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./load-balancer.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz load-balancer<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/load-balancer.gif" alt="Load Balancer, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* load-balancer: requests arriving and being spread across four servers, with live counters. */
import { Grid, palette, rng } from "../lib/kit.js";
export const meta = {
slug: "load-balancer",
name: "Load Balancer",
category: "explainers",
note: "Requests streaming into a load balancer and fanning out across four servers, each with its own request counter and connection bar.",
tags: ["load-balancer", "servers", "devops", "diagram", "infrastructure"],
cols: 90, rows: 25, fps: 24,
palette: palette(["#e6edf3", "#8b949e", "#30363d", "#79c0ff", "#7ee787", "#ffa657", "#d2a8ff", "#ffffff", "#6e7681", "#f2cc60", "#ff7b72"]),
};
const CORNER = { RD: "╮", RU: "╯", LD: "╭", LU: "╰", DR: "╰", DL: "╯", UR: "╭", UL: "╮" };
const dirOf = (a, b) => (b[0] > a[0] ? "R" : b[0] < a[0] ? "L" : b[1] > a[1] ? "D" : "U");
function cellsOf(pts) {
const out = [pts[0]];
for (let i = 1; i < pts.length; i++) {
let [x, y] = out[out.length - 1];
const [tx, ty] = pts[i];
while (x !== tx || y !== ty) { x += Math.sign(tx - x); y += Math.sign(ty - y); out.push([x, y]); }
}
return out;
}
const SERVERS = [["web-1", 1], ["web-2", 7], ["web-3", 13], ["web-4", 19]];
const TYPES = [["GET", "4"], ["GET", "4"], ["POST", "3"], ["GET", "4"], ["PUT", "6"], ["GET", "4"], ["DEL", "a"]];
export default function create() {
const { cols, rows } = meta;
const g = new Grid(cols, rows);
const LBY = 12, SX = 64;
const inbound = cellsOf([[0, LBY], [21, LBY]]);
const outs = SERVERS.map(([, y]) => cellsOf([[36, LBY], [46, LBY], [46, y + 2], [SX - 1, y + 2]]));
const r = rng(12);
const N = 120, GAP = 0.18, LOOP = N * GAP;
const REQ = [];
const busy = [0, 0, 0, 0];
for (let k = 0; k < N; k++) {
const t0 = k * GAP + (r() - 0.5) * 0.08;
const ty = TYPES[Math.floor(r() * TYPES.length)];
// least connections, ties round robin
let best = k % 4;
for (let s = 0; s < 4; s++) if (busy[s] < busy[best]) best = s;
busy[best] += 1 + r() * 0.8;
for (let s = 0; s < 4; s++) busy[s] = Math.max(0, busy[s] - 0.25);
REQ.push([t0, best, ty]);
}
const IN = 0.7, OUT = 0.8, WORK = 0.9;
return (t) => {
g.clear();
const lt = t % LOOP;
// wires
for (let x = 0; x <= 21; x++) g.set(x, LBY, "─", "8");
outs.forEach((cells) => {
for (let n = 0; n < cells.length; n++) {
const [x, y] = cells[n];
const din = n ? dirOf(cells[n - 1], cells[n]) : "R";
const dout = n < cells.length - 1 ? dirOf(cells[n], cells[n + 1]) : "R";
g.set(x, y, din === dout ? (din === "R" || din === "L" ? "─" : "│") : CORNER[din + dout], "8");
}
});
g.set(46, LBY, "┼", "8");
g.set(46, 9, "├", "8"); g.set(46, 15, "├", "8");
const served = [0, 0, 0, 0], active = [0, 0, 0, 0];
for (const [t0, s, [ty, co]] of REQ) {
for (const off of [0, -LOOP]) {
const age = lt - t0 - off;
if (age < 0) continue;
if (age < IN) {
const c = inbound[Math.max(0, Math.min(inbound.length - 1, Math.floor((age / IN) * inbound.length)))];
g.set(c[0], c[1], "●", co);
} else if (age < IN + OUT) {
const cells = outs[s];
const c = cells[Math.max(0, Math.min(cells.length - 1, Math.floor(((age - IN) / OUT) * cells.length)))];
g.set(c[0], c[1], "●", co);
} else if (age < IN + OUT + WORK) active[s]++;
if (off === 0 && age >= IN + OUT) served[s]++;
}
}
// load balancer box
g.box(22, LBY - 3, 14, 7, "double", "9");
g.text(25, LBY - 2, "balancer", "0");
g.text(24, LBY, "least conn", "1");
const rps = Math.round(100 / GAP);
g.text(25, LBY + 2, rps + " req/s", "9");
g.text(1, LBY - 2, "incoming", "1");
g.text(1, LBY - 1, "requests", "1");
// servers
SERVERS.forEach(([name, y], s) => {
const hot = active[s] >= 3;
g.box(SX, y, cols - SX - 1, 5, "round", hot ? "5" : "4");
g.text(SX + 2, y + 1, name, "0");
g.set(SX + 9, y + 1, "●", hot ? "5" : "4");
g.text(SX + 2, y + 2, "served", "1");
g.text(SX + 9, y + 2, String(1000 * (s + 2) + served[s]).padStart(6), "7");
g.text(SX + 2, y + 3, "conns", "1");
const bw = 8;
for (let k = 0; k < bw; k++) g.set(SX + 9 + k, y + 3, k < active[s] * 2 ? "■" : "□", k < active[s] * 2 ? (hot ? "5" : "4") : "2");
});
// legend
g.text(1, rows - 1, "●", "4"); g.text(3, rows - 1, "GET", "1");
g.text(8, rows - 1, "●", "3"); g.text(10, rows - 1, "POST", "1");
g.text(16, rows - 1, "●", "6"); g.text(18, rows - 1, "PUT", "1");
g.text(23, rows - 1, "●", "a"); g.text(25, rows - 1, "DELETE", "1");
const tot = served.reduce((a, b) => a + b, 0);
g.text(cols - 22, rows - 1, "total " + String(14000 + tot).padStart(6), "1");
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.