← Home
petri-net what-if-analysis simulation sim-pflow-xyz cost-benefit-analysis

The Capacity Paradox

A product team is arguing about headcount. The backlog is long, half of what's in it never ships, and someone wants to hire two more engineers to clear it faster. Someone else thinks the backlog is the problem, not the team size — too much gets approved that shouldn't have been. Both sides have a spreadsheet. Neither spreadsheet can tell you which one is right, because a spreadsheet can't simulate what happens when you add a person to a system that already has feedback loops in it: an evaluation gate, a build queue, a measurement window, engineers who get pulled onto things that later get killed.

sim.pflow.xyz is the same what-if engine we've pointed at a help desk and a vet clinic, and nothing in it is specific to engineering orgs. "Should I hire another engineer" and "should I hire another vet tech" have the same shape: a flow with a cost, a benefit, and a knob someone wants to turn without knowing what it does to the outcome. Whether the engine can say what that knob does is the part this post has to earn, and on a re-read it has earned less of it than the first version claimed (see What Changed).

A model of the argument

feature-lab is a Petri net of exactly the debate above: ideas land in a backlog (five to start), a pool of two analysts scores each one against cost and benefit, roughly a third get approved and go to an engineering pool of four, and every shipped feature spends a measurement window in telemetry before it's marked validated or killed — with the kill rate (0.02) twice the validate rate (0.01), about two in three get killed. Validated features seed a follow-on idea back into the backlog, the loop that makes this build-measure-learn rather than a one-shot pipeline. A meter, eng_hours, is meant to count engineer-hours spent building, including on the features that get killed later — the sunk cost the "just hire more" argument usually doesn't put a number on.

As built, the meter doesn't count that. It is driven by a read arc, in_development → meter_build → eng_hours, and a read arc tests for a token without consuming it, so the meter bills one engineer-hour an hour whenever anything is in development, however many features are in build and however many engineers are on them. The sim project tracks this as an open defect — the same read-arc meter was found and rewritten in its delivery-fleet model — and feature-lab still needs the rewrite.

Every model in the catalog gets the same automated pass: diagnose ranks every knob — every pool, every source, every patience threshold — by its measured effect on the model's declared objective rather than by intuition about which one should matter. Run once against feature-lab's objective (validated − 0.5·killed − 0.02·eng_hours), it reported that past a certain team size adding engineers makes the objective worse, and the explanation offered was that a loose evaluation gate lets the extra capacity burn engineer-hours on features that were always going to be killed, so the fix is a tighter gate rather than more people. That is the second side of the argument, and it is not established. The run wasn't preserved. The engineer-hours in the explanation are exactly what the broken meter can't charge more for — with four engineers or forty it bills the same hour — so whatever lowered the score in that run, the killed term is the only cost that grows with throughput. And we never ran the tighter-gate comparison at all. sim's own landing page now marks the finding unconfirmed until the meter is rebuilt, which is the right status for it here too.

The same shape elsewhere in the catalog

The first version of this post leaned hardest on what came next: the same result shape turning up across models that were never built to make the point, when the same pass ran over the rest of the catalog. That sweep ran once, in a chat session, over what was then a 28-model catalog. It isn't preserved as a script or a recorded run, and it hasn't been re-run for this revision. Read the table as what that one pass reported. Each model links to the what-if console, where Measure influence runs the same check.

Model The resource everyone would assume matters What that diagnose run reported
ward-flow Two more ED bays — the question the model's own description asks Neither ED bays nor ward beds move the needle; raw arrival rate is the only live knob
launch-plan More engineers — the actual build resource engineer_free is inert; how fast briefs get pulled off the queue is what binds
invoice-collections More collectors and more account managers Both inert; only demand and write-off timing matter
loan-book More underwriters Inert — the collections desk is the lever, not underwriting
autoscaler More compute (instance_slots) Inert — the pre-warmed pool (warm) is what cuts cold-start loss
vet-clinic 8 of 11 specialty rooms and pieces of equipment Inert — front-of-house staffing (techs, vets, reception) is what binds

Two things in the engine argue for caution about the word inert. diagnose measures each knob at the model's own operating point, so a knob that only matters under load reads inert at calm — retry-storm's description says exactly this about its give_up rate, which "reads inert in diagnose because at the calm operating point the storm regime is never entered." And sim has an open issue that on models with a declared objective, capacity that measurably costs money can be labelled inert because the adaptive sample never escalates. So inert in the table means no effect measured at the default marking in that run, not proven useless. Two rows have backing outside the sweep: ward-flow's ED-bay finding is still on sim's landing page as a check anyone can repeat, and the vet-clinic direction matches a fitness gate in sim's own source — vetclinic_fitness_test.go (in pkg/runtime/sim; the repository is private) asserts that no room pool outranks every staff pool in the contention ranking (not re-run here; What-If Analysis has the detail).

The one case of this shape with preserved numbers is the café: over an 8-hour day, 24 seeded runs per measurement, 97 of about 267 customers walk out at two baristas, a third barista saves about 47 of them, and pantry stock and restocking stay inside the ±2.9-walkout noise floor. Those figures are sim's, re-measured against the seeded café when its landing page was rebuilt; we didn't re-run them. Open the café and press Measure influence to repeat them.

None of these were built to make a point about capacity — an autoscaler, a hospital ward, a lending desk, a launch pipeline, each asking its own question — which is why the pattern would matter if a preserved re-run holds it up. Until then it is a pattern one pass reported and the café confirms once.

The first version also cited retry-storm as a dead feature: structural analysis proves its traffic-spike arrival, spike_arrive, can never fire from the model's starting state. The analysis is right — spike_pool has no input arcs and starts empty, an unmarked siphon, so nothing inside the net can ever put a token in it — but calling it a defect was wrong. The model's own description says spike_pool is a burst reservoir, zero at calm, that a scenario fills (it suggests injecting 50) to compare aggressive retries, capped retries and doubled servers. What the check caught is a place whose only source is outside the net. That's still worth flagging on every model, because sometimes it is a bug; here it's the design.

Next to RICE and WSJF

Cost-benefit analysis for software decisions has always been done — RICE scores, WSJF, decision trees with expected value, Monte Carlo throughput forecasts. Each is a real technique, and each asks its question at a single point in time, on a system that doesn't talk back. None of them simulates the queue, the feedback loop and the sunk cost at once from one declared model, which is what it would take to see an evaluation gate and a headcount pulling against each other. feature-lab is the attempt to do that, as a different instrument pointed at the same decision rather than a replacement for those techniques. Once its meter charges per engineer, the claim this post is named for gets its real test.

The engine doesn't care whether the flow is customers through a café or ideas through a backlog; cost-benefit analysis for anything you can describe as a flow was sim.pflow.xyz's headline when this post was written. Connect any MCP client to https://sim.pflow.xyz/mcp — sim_diagnose is one of the public tools that need no sign-in — describe your own team's backlog the way feature-lab describes this one, and ask sim_diagnose what it finds. For the operational side of the same instrument — staffing, queues, disruptions — see The Model Is the App and What-If Analysis.

What Changed

For readers of the September version (revised 2026-10-09):

  1. feature-lab's finding is unconfirmed. Its eng_hours meter runs off a read arc and bills one hour an hour regardless of headcount, so it can't carry the "extra capacity burns engineer-hours" explanation. The run behind the finding wasn't preserved, and the tighter-gate fix was never tested. The post said diagnose "found the second side of the argument was right"; it now says what one run reported and why that isn't proof.
  2. The catalog table is one unpreserved sweep. Added links to each model, the two caveats on inert (measured at the calm operating point; an open sim issue on objective models), the two rows with independent backing, and the café as the one case with recorded numbers.
  3. retry-storm is not a dead feature. spike_pool is a deliberately empty, scenario-filled reservoir; the structural finding is correct, reading it as a defect was not.
  4. Smaller: the earlier "vet clinics and help desks" link pointed only at the help-desk post; the closing claim that "growing the team won't fix it" is withdrawn with the finding it rested on; sim's headline has since changed, so the post no longer calls the flow framing its current thesis.
×

Follow on Mastodon