Skip to content

Case study: a 100k-client streaming CDN network

A complete, end-to-end worked example of the YourSimulation plugin: take a vague real-world ask — "model a 100,000-client streaming network spread across 30 CDN points" — and turn it into a validated model, a runnable experiment, and an analysed result. Along the way it shows the two ideas that matter most when modelling at scale: arrivals are a rate, not a node and queueing KPIs are scale-invariant.


1. What the plugin does

YourSimulation is a discrete-event queue simulator driven from a single JSON model. The workflow is always the same four steps:

StepCommandPurpose
AuthorDescribe the system as a graph of nodes + edges (or generate it).
Validateyoursim validate model.jsonCatch structural errors before running.
Runyoursim run model.json --prettySimulate and emit KPIs (mean + 95% CI).
Optimizeyoursim optimize model.json problem.jsonFind the cheapest design meeting a target.

The engine simulates entities (here: streaming sessions) flowing through generic node types. This case study uses four of them: source (arrivals), branch (routing), queue (waiting line), resource (servers), and sink (exit).

The CLI bin is named yoursim. When the package is installed locally, invoke it as node node_modules/@plantagoai/yoursim-engine/dist/cli.js <cmd> (or via the yoursim bin on your PATH).


2. From a sentence to parameters

"100k clients across 30 CDNs" is under-specified. Four questions pin it down:

QuestionAnswer chosenModelling consequence
What is "100k clients"?100,000 stream-starts per secondsource inter-arrival mean = 1/100000 = 0.00001 s… (see scaling)
How long is a session?~45 min (exp, mean 2,700 s)resource service distribution
How are clients routed?Evenly across 30 CDNsbranch mode probability, 1/30 per edge
What capacity / goal?Size each CDN for stabilitychoose servers so utilization < 1

The first big idea: arrivals are a rate

A naïve model would create one node per client — a million nodes. Wrong. In discrete-event simulation, arrivals are a distribution on one source. 100,000 clients/sec is a single source with a tiny inter-arrival mean, not 100,000 nodes. The whole network is 63 nodes regardless of client count.

Sizing each CDN (Little's Law)

The offered load on the system is arrivals × holding time:

L=λ×W=100,000/s×2,700s=270,000,000 concurrent streams

Split evenly over 30 CDNs → 9,000,000 concurrent streams per CDN (9,000,000 erlangs of offered load). A resource is stable only when servers > λ·service, so each CDN needs > 9M slots. Sizing for ~90% utilization gives 10,000,000 slots per CDN, 300,000,000 total.


3. The second big idea: scale-invariance

At literal full scale the simulation is intractable. To reach steady state the warmup must exceed one 45-min session (~2,700 s); at 100,000 arrivals/sec that is 270 million arrivals before measurement even begins — billions of events. No machine finishes that in useful time.

But queueing KPIs — utilization, p95 wait, blocking probability — depend only on the offered-load ratio:

ρ=λserviceservers

not on absolute size. So we simulate a ÷1000 rate-scaled twin: divide the arrival rate (and the slot counts) by 1000. The ratio ρ is identical, so utilization and waits come out the same; only counts (throughput, concurrency) scale, and we multiply them back by 1000 to read full-scale numbers.

QuantityFull scaleSimulated twin (÷1000)Read back
Stream-starts100,000 /s100 /s (inter-arrival 0.01 s)rate ×1000
Slots per CDN10,000,00010,000×1000
Concurrent streams270 M (offered)270,000×1000
Utilization, p95 waitidentical×1

4. Model construction

Thirty near-identical CDN branches are best generated, not hand-written. The generator computes the slot count from the load math so the model stays correct if you change the CDN count or scale factor.

ts
// Generate a streaming-CDN network model and print JSON to stdout.
//
// This is the "rate-scaled twin" of a 100k stream-starts/sec network held
// across 30 CDN edge points, with ~45-min sessions, even regional routing.
// Arrival rate is scaled down by SCALE so the discrete-event sim is tractable
// (every arrival is an event; warmup must exceed one ~45-min session).
// KPIs (utilization, p95 wait, blocking) are scale-invariant; multiply
// throughput / concurrency COUNTS by SCALE to read back to full scale.
//
// Run: npx tsx generate-cdn-network.ts [numCdns] [scale] > cdn-network.json
//   numCdns : number of CDN edge points (default 30)
//   scale   : divide full-scale arrival rate by this (default 1000)

const FULL_RATE = 100_000;            // stream-starts / sec at full scale
const SESSION_MEAN = 2700;            // ~45 min, in seconds
const TARGET_UTIL = 0.9;              // size each CDN to ~90% utilization

const numCdns = Number(process.argv[2] ?? 30);
const scale = Number(process.argv[3] ?? 1000);

const lambdaTotal = FULL_RATE / scale;              // scaled total arrival rate
const interarrival = 1 / lambdaTotal;               // mean seconds between starts
const lambdaPerCdn = lambdaTotal / numCdns;         // even regional split
const offeredPerCdn = lambdaPerCdn * SESSION_MEAN;  // erlangs (concurrent load)
const serversPerCdn = Math.ceil(offeredPerCdn / TARGET_UTIL);

const nodes: unknown[] = [
  {
    id: 'clients', type: 'source', label: `Clients (${lambdaTotal}/s, twin of ${FULL_RATE}/s)`,
    position: { x: 0, y: 0 },
    params: { interarrival: { dist: 'exp', mean: interarrival } },
  },
  {
    id: 'steer', type: 'branch', label: 'Regional steering (even)',
    position: { x: 250, y: 0 }, params: { mode: 'probability' },
  },
  { id: 'served', type: 'sink', label: 'Session complete', position: { x: 900, y: 0 }, params: {} },
];

const edges: unknown[] = [
  { id: 'e-src', from: 'clients', to: 'steer' },
];

for (let i = 0; i < numCdns; i++) {
  const q = `cdn${i}-q`, r = `cdn${i}`;
  nodes.push({
    id: q, type: 'queue', label: `CDN ${i} buffer`,
    position: { x: 500, y: i * 50 }, params: { discipline: 'fifo' },
  });
  nodes.push({
    id: r, type: 'resource', label: `CDN ${i} edge (${serversPerCdn} slots)`,
    position: { x: 700, y: i * 50 },
    params: { servers: serversPerCdn, service: { dist: 'exp', mean: SESSION_MEAN } },
  });
  edges.push({ id: `e-steer${i}`, from: 'steer', to: q, probability: 1 / numCdns });
  edges.push({ id: `e-q${i}`, from: q, to: r });
  edges.push({ id: `e-r${i}`, from: r, to: 'served' });
}

process.stdout.write(JSON.stringify({
  schemaVersion: 1,
  name: `Streaming CDN network (${numCdns} edges, 1/${scale} twin of ${FULL_RATE}/s)`,
  settings: { timeUnit: 'sec', horizon: 11000, warmup: 8000, replications: 4, seed: 42 },
  presentation: { theme: 'network' },
  nodes, edges,
}, null, 2));

Run it and validate:

bash
npx tsx generate-cdn-network.ts 30 1000 > cdn-network.json
yoursim validate cdn-network.json          # -> {"ok":true}

Topology

                    ┌─ cdn0-q  → cdn0  (10k slots) ─┐
clients ─► steer ──┼─ cdn1-q  → cdn1  (10k slots) ─┼─► served
(100/s)  (1/30 ea) ├─  …  ×30                       │   (sink)
                    └─ cdn29-q → cdn29 (10k slots) ─┘

Every resource is fed by its own queue (an engine validation rule — entities need somewhere to wait). Routing is an even probability branch. The full generated model (63 nodes / 91 edges) is in docs/examples/cdn-network.json; a representative excerpt — the source, the router, one CDN branch, and the sink (full file on GitHub):

json
{
  "schemaVersion": 1,
  "name": "Streaming CDN network (30 edges, 1/1000 twin of 100000/s)",
  "settings": { "timeUnit": "sec", "horizon": 11000, "warmup": 8000, "replications": 4, "seed": 42 },
  "nodes": [
    { "id": "clients", "type": "source", "params": { "interarrival": { "dist": "exp", "mean": 0.01 } } },
    { "id": "steer",   "type": "branch", "params": { "mode": "probability" } },
    { "id": "cdn0-q",  "type": "queue",  "params": { "discipline": "fifo" } },
    { "id": "cdn0",    "type": "resource", "params": { "servers": 10000, "service": { "dist": "exp", "mean": 2700 } } },
    { "id": "served",  "type": "sink", "params": {} }
  ],
  "edges": [
    { "id": "e-src",    "from": "clients", "to": "steer" },
    { "id": "e-steer0", "from": "steer",   "to": "cdn0-q", "probability": 0.03333 },
    { "id": "e-q0",     "from": "cdn0-q",  "to": "cdn0" },
    { "id": "e-r0",     "from": "cdn0",    "to": "served" }
  ]
}

Run settings

warmup: 8000 s lets the 45-min-session pipe fill before stats are collected; horizon: 11000 s then measures a 3,000 s steady-state window. replications: 4 gives a mean + 95% CI. The run took ~202 s on engine @plantagoai/yoursim-engine v0.1.2 (results are deterministic for a fixed seed).


5. Results

bash
yoursim run cdn-network.json --pretty > kpis.json

Verdict — the network comfortably holds the load. Every CDN runs ~87% busy with zero queueing: no waits, no buffer build-up, no dropped sessions. ~13% headroom.

Headline KPIs

KPISimulated (twin)Full scale (×1000)Notes
Stream-starts99.86 /s99,860 /smatches the 100k/s target
Completed throughput96.92 /s96,920 /sgap = sessions still in-flight at window end
Mean CDN utilization87.1 %87.1 %target 90% (see §6)
Queue wait (avg / p95)0 s / 0 s0 s / 0 sno congestion anywhere
Dropped / balked / reneged00no losses
Concurrent streams261,351261.4 Mof 300 M provisioned

Per-CDN utilization — even, no hot spots

All 30 edges land in a tight 86.2 %–88.2 % band, confirming the even router:

cdn13  ██████████████████████████████████  86.2 %   ← lowest
cdn00  ███████████████████████████████████ 87.2 %
cdn14  ███████████████████████████████████ 88.2 %   ← highest
       (all 30 cluster around mean 87.1 %)

spread  86.0   86.5   87.0   87.5   88.0   88.5 %
        ░░░░▓▓▓▓████████████▓▓▓▓▓░░░
              min 86.2   mean 87.1   max 88.2

Time-in-system = the streaming session itself

Because queue wait is zero, end-to-end time equals the exp(2,700 s) session — a workload property, not a congestion signal:

time       0    1500   3000   4500   6000 s
p50  29m   ███████████
mean 40m   ███████████████
p90  87m   █████████████████████████████████
p95 106m   ████████████████████████████████████████
MetricObservedexp(2700) theory
p501,753 s1,872 s
p905,245 s6,217 s
p956,381 s8,091 s
mean (completed)2,404 s2,700 s

Capacity read-back (full scale)

QuantityValue
Slots provisioned300.0 M
Concurrent streams (steady state)261.4 M
Headroom12.9 %
Offered load (100k/s × 2,700 s)270 M erlangs

6. Analysis

Why zero queueing? Each CDN is an M/M/c queue with c = 10,000 and ρ ≈ 0.87. The probability that all 10,000 servers are simultaneously busy (Erlang-C) is vanishingly small at this scale, so an arriving session almost always finds a free slot. Latency is therefore pure streaming time, not waiting time. A large, well-provisioned system barely queues even at high utilization — the opposite of a small M/M/1, where 87% utilization means long, volatile waits.

Why 87% and not the 90% it was sized for? The 45-min sessions fill the pipe slowly; at the 8,000 s warmup boundary the system was ~97% saturated. A longer warmup converges to 90% at higher run cost — a fidelity/runtime trade-off, not a model error.

Why throughput < arrivals and mean-TIS < 2,700 s? Both are right-censoring artifacts of observing 45-min sessions through a 3,000 s window: sessions still streaming at the window's end aren't counted as completed. The queues are provably empty (avg length and p95 wait are 0 on every CDN), so this is a measurement effect, not instability.

Recommendations

  • Design is sound. 10M slots/CDN × 30 edges serves 100k/s of 45-min sessions with no queueing and ~13% headroom.
  • Trim cost. At 87% you can drop to ~9.5M slots/CDN (285 M total) and still clear ~95% utilization with negligible wait. Use optimize with a p95-wait constraint to find the cheapest slot count.
  • Stress test. To find the breaking point, fix a lower capacity (e.g. 9M/CDN) or switch the router to a skewed probability split to expose hot-spot CDNs.

7. Reproduce it

bash
# 1. generate the ÷1000 twin (30 CDNs)
npx tsx generate-cdn-network.ts 30 1000 > cdn-network.json
# 2. validate
yoursim validate cdn-network.json
# 3. run -> KPIs
yoursim run cdn-network.json --pretty > kpis.json

Files: docs/examples/cdn-network.json · docs/examples/generate-cdn-network.ts. Change the args to explore: generate-cdn-network.ts 50 500 models 50 CDNs at a ÷500 twin.