loadmap 0.1 npm GitHub
Theme

Start

Concepts

A loadmap is a model (plain data and functions, no React), a view (which boxes to draw), and the page that shows both. Everything a page says comes from the model, so a test or loadmap check can say the same without a browser.

On this page

The load and the parameters

The load is a multiple of today: 1× is the busy period you measured, 10× is ten times that. Parameters are what a reader may change: a number of pods (p.select), a switch (p.switch), a range (p.range). A preset is a named set of values offered as a button ("Black Friday": 40× with 6 pods).

Flows and components

A flow is something that moves through the system at a rate that depends on the load: orders per second, events, files. A component is something that can run out: its demand (what it must handle, from the flows) against its capacity. Every capacity says where it came from:

The page lists every one of them under Assumptions, so a reader can see which answers rest on guesses.

Limits and the verdict

Each component's utilization is demand ÷ capacity. Two thresholds matter: the target (50 % by default, the headroom you want to keep) and full (100 %). For each component loadmap finds the load multiple where it crosses each one, ranks them, and writes the verdict: "At 10×, every component is under the 50 % target. It holds up to 14×; the worker pool is the first limit."

With a ± on a capacity, every crossing becomes a range ("between 7.4× and 10×"). The ranking, the first limit and every check use the pessimistic edge, every capacity at its low bound, where it may already happen. When a range is wide, a line under the verdict names the capacity whose measurement would change the answer most.

Stages: what the dots do

Stages say how each flow moves, so the dots behave like the system: pass (rejects what is above capacity), queue (partitions that fill up and drain), workers (pods × threads that take work), store (where a flow ends), sink (a receiver), fanout (one unit becomes several). A route lists the stages a flow passes. The simulation never has numbers of its own: a stage drains exactly what its component can take at 100 %, so the dots and the bars always agree.

Scenarios, growth and the headroom chart

A scenario is a named design: parameter values without the load ("Proposed": 2 gateways, 6 pods). The page shows a button per scenario, the scenarios side by side, and a headroom chart: utilization over the load multiple, one line (or band) per scenario. With growth ({ rate: 0.06, per: 'month', asOf: '2026-11-01' }), multiples become dates: "around March 2031". The date is counted from asOf, never from today, so the page and its recordings say the same thing tomorrow.

Expectations and checks

An expectation is a promise: "Today keeps 7×", "the database never fills before 100×". check(model) tests them on the pessimistic edge, assertHolds(model) throws in a test, and loadmap check model.loadmap exits 1 in CI. The editor marks a broken one on its line.

A story

A story is a list of steps, each with a text, values and a component to point at. The page shows a bar with back and next; loadmap record --story plays it with the text under the board. Each step starts from the defaults, so it looks the same whichever step came before.

The view and the layout

The view names the boxes (node, cluster for brokers holding partitions, pool for pods), the edges and their labels, and where each flow's dots start. ELK lays out each scenario, step and choice of a select or switch on its own, and the boxes move from one to the next: a label reserves room for its widest text and a box for its largest size across the whole load range and every range control, so dragging a slider never moves a box, and no label covers another. layout: 'stable' gives every box one place instead, so nothing but a box's own size changes between steps.

The diagram runs right or down by its shape, unless the view says which: right when its longest chain is at least as long as its widest layer, down when the widest layer is wider. Here a gateway feeds five services that share one database, so it runs down, even on a wide screen, and its tiers become lanes with their names up the left:

loadmap 1
title: "Five services, one database"
load "Requests, × today" { max: 20; default: 1 }

flow search "search" { rate: 400 * load; route: gateway -> search -> db; from: users }
flow cart "cart" { rate: 150 * load; route: gateway -> cart -> db; from: users }
flow orders "orders" { rate: 60 * load; route: gateway -> orders -> db; from: users }
flow profile "profile" { rate: 100 * load; route: gateway -> profile -> db; from: users }
flow stock "stock" { rate: 80 * load; route: gateway -> stock -> db; from: users }

tier edge "Edge"
tier apps "Apps"
tier data "Data"

node users "Users"
node gateway "Gateway" {
  tier: edge
  pass
  capacity: measured(10_000, "req/s", "load test")
  over: "clients get 429s"
}
node search "Search" { tier: apps; pass; capacity: measured(4_000, "req/s", "load test"); over: "requests queue" }
node cart "Cart" { tier: apps; pass; capacity: measured(2_000, "req/s", "load test"); over: "requests queue" }
node orders "Orders" { tier: apps; pass; capacity: measured(1_000, "req/s", "load test"); over: "requests queue" }
node profile "Profile" { tier: apps; pass; capacity: measured(2_000, "req/s", "load test"); over: "requests queue" }
node stock "Stock" { tier: apps; pass; capacity: measured(1_500, "req/s", "load test"); over: "requests queue" }
node db "Database" {
  tier: data
  shape: cylinder
  store
  capacity: estimated(3_000, "queries/s", "vendor sizing")
  over: "queries queue"
}

users -> gateway
gateway -> search -> db
gateway -> cart -> db
gateway -> orders -> db
gateway -> profile -> db
gateway -> stock -> db

As text

Everything above can be written as a .loadmap definition instead of TypeScript, and edited next to its live diagram. This one is drawn from its text right here:

loadmap 1
title: "A gateway and a worker pool"
load "Users, × today" { max: 100; default: 10 }
param pods "Worker pods" { options: [2, 3, 4, 6]; default: 3 }
scenario "Today" {}
scenario "Proposed" { pods: 6 }

flow orders "orders" { rate: 40 * load; route: gateway -> workers; from: phones }

node phones "Phones"
node gateway "API gateway" {
  pass
  short: "the API gateway"
  demand: flows.orders
  capacity: estimated(6_000 ± 30%, "req/s", "vendor benchmark")
  over: "clients get 429s"
}
pool workers "Workers" `${pods} pods × 8 threads` {
  workers { pods: pods; perPod: 8 }
  short: "the workers"
  plural: true
  demand: flows.orders * 0.004
  capacity: measured(pods * 2, "cores", "pod CPU limit")
  over: "messages wait and the lag keeps growing"
}

phones -> gateway -> workers

The language describes every statement.