---
title: 'Flow-Based Management'
source: 'https://academia.sh/en/courses/process-and-team/flow-based-management'
course: 'Process, Team and Delivery'
language: en
updated: '2026-08-23T07:01:07+00:00'
license: 'CC BY-SA 4.0'
---

# Flow-Based Management

Measuring work-in-progress limits and flow metrics: lead time, cycle time, and throughput are defined in the same run, the WIP limit is scanned from one to sixteen, and idle capacity below the limit is counted against work effort lost to switching above it.

Every measurement in the previous lesson rested on a rhythm assumption: a work item can only start
at the next planning session and can only be accepted at the end of the iteration. Once the rhythm is
removed, no batch remains, and a work item is pulled the moment it is ready. What is left is a single
control lever — **how many items are open at the same time**. This lesson scans that number from one
end to the other.

For the scan to mean anything, the three metrics must first be separated. **Lead time** is the total
duration from the day an item is requested to the day it reaches operations. **Cycle time** is the
duration from the day an item is pulled into work to the day it finishes — lead time with the portion
spent queued removed. **Throughput** is the number of items finished per unit of time. All three come
out of the same run, and each points to a different decision. A work-in-progress (WIP) limit is a
backpressure lever; because queueing mechanics and backpressure are measured elsewhere in this
curriculum, what is counted here is not the queue itself but the limit's effect on human flow. The
through-line is the regional library network, and it is fiction.

## Three Metrics, One Run

The block below is a **model**, not a measurement.

**PM13 — sixty work items are derived from a generator; the generator is self-written and the seed is
visible.** Every item has a size (2–8 person-days) and an arrival day; the same set is used at every
limit value.

**PM14 — an item can receive at most 1 person-day per day**, because only one person works on it. This
is the rule that decides what is lost below the limit: with two items open, a four-person team can
spend at most 2 person-days a day, and the remaining two people sit idle.

**PM15 — once the number of open items exceeds the number of people, part of the day goes to
switching.** Every extra open item per person takes away 15% of that day's capacity. This is the rule
that decides what is lost above the limit.

**PM16 — the WIP limit caps the number of open items.** An item that arrives while the limit is full
waits in the queue, and that wait counts toward lead time, not cycle time.

**PM17 — items are pulled in arrival order, and an item once started is never set aside
half-finished.** Changing the priority order would change the distribution of lead time; holding it
fixed ensures the only thing that changes in the scan is the limit itself.

```js
// flow.mjs — work-in-progress-limited pull system (model); flow metrics defined at a single limit
import { writeFileSync } from "node:fs";

const TEAM = 4;         // team
const PENALTY = 0.15;   // daily loss per person for every extra open item

// PM13: the generator is self-written, the seed is visible; the same item set is used at every limit.
let seed = 20250715;
const rand = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
let t = 0;
const ITEM = [...Array(60)].map((_, id) => {
  t += rand() < 0.35 ? 0 : 1;                  // arrivals average around 1.5 items per day
  return { id, size: 2 + Math.floor(rand() * 7), arrival: t };
});

function run(wip) {
  const queue = ITEM.map((k) => ({ ...k, remaining: k.size, start: -1, end: -1 }));
  const open = [];
  let day = 0, idleCapacity = 0, switchingLoss = 0;
  while (queue.some((k) => k.end < 0)) {
    day += 1;
    for (const k of queue)                      // the WIP limit's slack pulls in items
      if (open.length < wip && k.start < 0 && k.arrival <= day) { k.start = day; open.push(k); }
    const n = open.length;
    if (n === 0) { idleCapacity += TEAM; continue; }
    // PM15: with more than one open item per person, part of the day goes to switching
    const effective = TEAM * (1 - PENALTY * Math.max(0, n / TEAM - 1));
    switchingLoss += TEAM - effective;
    const share = Math.min(1, effective / n);    // PM14: an item can get at most 1 person-day per day
    idleCapacity += Math.max(0, effective - share * n);
    for (const k of [...open]) {
      k.remaining -= share;
      if (k.remaining <= 1e-9) { k.end = day; open.splice(open.indexOf(k), 1); }
    }
  }
  const measurements = queue.map((k) => ({
    id: k.id, size: k.size, leadTime: k.end - k.arrival + 1, cycle: k.end - k.start + 1,
    wait: k.end - k.start + 1 - k.size, queued: k.start - k.arrival,
  }));
  const avg = (f) => measurements.reduce((a, x) => a + f(x), 0) / measurements.length;
  return { wip, day, measurements,
    leadTime: avg((x) => x.leadTime), cycle: avg((x) => x.cycle), queued: avg((x) => x.queued),
    waitShare: avg((x) => x.wait) / avg((x) => x.cycle),
    throughput: ITEM.length / day, idleCapacity: +idleCapacity.toFixed(1), switchingLoss: +switchingLoss.toFixed(1) };
}

const SCAN = [1, 2, 3, 4, 5, 6, 8, 10, 12, 16].map(run);
writeFileSync("scan.json", JSON.stringify(SCAN.map(({ measurements, ...r }) => r)));

const five = SCAN[4];
console.log(`work items: ${ITEM.length}, base work: ${ITEM.reduce((a, k) => a + k.size, 0)} person-days, ` +
  `last arrival: day ${ITEM[ITEM.length - 1].arrival}, team ${TEAM} (seed ${20250715})`);
console.log(`\nWIP limit ${five.wip}: three metrics on the first eight items (days):`);
console.log(`${"item".padStart(6)}${"size".padStart(7)}${"queued".padStart(10)}` +
  `${"cycle time".padStart(15)}${"of which wait".padStart(17)}${"lead time".padStart(15)}`);
for (const x of five.measurements.slice(0, 8))
  console.log(`${String(x.id).padStart(6)}${String(x.size).padStart(7)}${String(x.queued).padStart(10)}` +
    `${String(x.cycle).padStart(15)}${String(x.wait).padStart(17)}${String(x.leadTime).padStart(15)}`);
console.log(`\nlead time = time queued + cycle time; cycle time = work + wait`);
console.log(`average: lead time ${five.leadTime.toFixed(1)}, cycle ${five.cycle.toFixed(1)}, ` +
  `queued ${five.queued.toFixed(1)} days; wait share ${(100 * five.waitShare).toFixed(1)}%; ` +
  `throughput ${five.throughput.toFixed(2)} items/day`);
```

```
work items: 60, base work: 285 person-days, last arrival: day 40, team 4 (seed 20250715)

WIP limit 5: three metrics on the first eight items (days):
  item   size    queued     cycle time    of which wait      lead time
     0      7         1              9                2             10
     1      7         0              9                2              9
     2      7         0              9                2              9
     3      5         0              7                2              7
     4      2         0              3                1              3
     5      2         2              3                1              5
     6      2         4              3                1              7
     7      3         3              4                1              7

lead time = time queued + cycle time; cycle time = work + wait
average: lead time 21.9, cycle 6.6, queued 15.3 days; wait share 27.8%; throughput 0.74 items/day
```

Item six shows in a single line why the metrics have to be kept apart: a two-person-day piece of work
finishes in three days once it is pulled into work, but it takes seven days from the day it was
requested. Cycle time says the item carries one-third of a day's wait; lead time says
**three-quarters** of the duration passed before work even began. In the same run, these two metrics
can be four times apart from each other.

## Scanning the WIP Limit

**PM18 — three metrics drive three separate decisions,** and the point of the scan is to see which one
points to which limit. The block below scans the limit from one to sixteen.

```js
// limit.mjs — reads the scan results flow.mjs wrote; the WIP limit is scanned end to end
import { readFileSync } from "node:fs";

const T = JSON.parse(readFileSync("scan.json", "utf8"));
const BASE = 285;
const table = (header, row, caption) => {
  const write = (h) => console.log(h.map((c, i) => String(c).padStart(header[i][1])).join(""));
  if (caption) { console.log(); console.log(caption); }
  write(header.map((b) => b[0])); row.forEach(write);
};

table([["WIP limit", 10], ["total days", 12], ["lead time", 15], ["cycle time", 15],
       ["queued", 10], ["wait share", 14], ["throughput", 12]],
  T.map((r) => [r.wip, r.day, r.leadTime.toFixed(1), r.cycle.toFixed(1), r.queued.toFixed(1),
                `${(100 * r.waitShare).toFixed(1)}%`, r.throughput.toFixed(2)]));

table([["WIP limit", 10], ["total capacity", 17], ["base work", 10], ["idle", 11],
       ["to switching", 14], ["idle share", 12]],
  T.map((r) => [r.wip, 4 * r.day, BASE, r.idleCapacity.toFixed(1), r.switchingLoss.toFixed(1),
                `${((100 * r.idleCapacity) / (4 * r.day)).toFixed(1)}%`]),
  "where the capacity goes (person-days):");

// PM18: three metrics drive three different decisions; each one points to a different limit.
const least = (f) => T.reduce((a, b) => (f(b) < f(a) ? b : a));
const peak = least((r) => -r.throughput), last = T[T.length - 1];
console.log(`\nshortest cycle time at limit ${least((r) => r.cycle).wip}, ` +
  `shortest lead time at limit ${least((r) => r.leadTime).wip}, ` +
  `highest throughput at limit ${peak.wip} (${peak.throughput.toFixed(2)} items/day)`);
console.log(`throughput peaks at limit ${peak.wip}, then falls: ` +
  `at limit ${last.wip} it is ${last.throughput.toFixed(2)} items/day and ${last.switchingLoss.toFixed(1)} person-days go to switching`);
console.log(`lost below the limit: at limit 1, ` +
  `${((100 * T[0].idleCapacity) / (4 * T[0].day)).toFixed(1)}% of capacity sits idle, ` +
  `at limit ${peak.wip} it is ${((100 * peak.idleCapacity) / (4 * peak.day)).toFixed(1)}%`);
```

```
 WIP limit  total days      lead time     cycle time    queued    wait share  throughput
         1         285          118.5            4.8     113.8          0.0%        0.21
         2         143           51.0            4.8      46.3          0.0%        0.42
         3          98           28.4            4.8      23.7          0.0%        0.61
         4          72           17.3            4.8      12.5          0.0%        0.83
         5          81           21.9            6.6      15.3         27.8%        0.74
         6          83           23.8            8.1      15.7         41.2%        0.72
         8          89           27.6           11.3      16.3         57.9%        0.67
        10          94           31.4           14.6      16.8         67.5%        0.64
        12         102           36.7           18.9      17.9         74.8%        0.59
        16         123           51.0           29.9      21.1         84.1%        0.49

where the capacity goes (person-days):
 WIP limit   total capacity base work       idle  to switching  idle share
         1             1140       285      855.0           0.0       75.0%
         2              572       285      287.0           0.0       50.2%
         3              392       285      107.0           0.0       27.3%
         4              288       285        3.0           0.0        1.0%
         5              324       285        5.0          11.4        1.5%
         6              332       285        4.0          23.6        1.2%
         8              356       285        7.0          49.2        2.0%
        10              376       285        4.0          75.7        1.1%
        12              408       285        4.0         109.1        1.0%
        16              492       285        5.0         195.9        1.0%

shortest cycle time at limit 1, shortest lead time at limit 4, highest throughput at limit 4 (0.83 items/day)
throughput peaks at limit 4, then falls: at limit 16 it is 0.49 items/day and 195.9 person-days go to switching
lost below the limit: at limit 1, 75.0% of capacity sits idle, at limit 4 it is 1.0%
```

## Which Metric Drives Which Decision

The table's most instructive row is the first one. **When the WIP limit is one, cycle time is
shortest at 4.8 days and wait share is exactly zero** — the single open item is worked without
interruption and without any wait at all. In the same row, lead time is 118.5 days and **75% of
capacity sits idle**. A flow managed by watching cycle time alone makes the worst possible system look
flawless.

When the limit rises to four, four people spread across four items, idle capacity drops from 75% to
**1%**, throughput rises from 0.21 to 0.83 items/day, and lead time falls from 118.5 to 17.3 days. Up
to this point cycle time has not degraded at all (4.8 days), because each item still has a full person
assigned to it.

Past a limit of five, a second mechanism kicks in. The moment more than one item falls to each person,
cycle time rises from 4.8 to 6.6 days and **wait share jumps from zero to 27.8%**. At sixteen, cycle
time is 29.9 days and wait share is 84.1%: five-sixths of an item's life passes with no one working on
it. Throughput peaks at four and drops to 0.49 at sixteen; 195.9 person-days go to switching. **Raising
the limit opens more items at once, but finishes none of them any sooner.**

The three metrics look at three separate decisions. Cycle time measures the flow inside the work
itself and shows where in the process wait accumulates; read alone, it hides everything sitting in the
queue. Lead time is the measure of the promise given to demand, and it drives the capacity decision.
Throughput says whether new work can be accepted, and the point where it saturates marks the upper end
of the limit. In this run, the three point to limits of one, four, and four respectively; reading only
one of them means missing two of the three.

## Summary

- Lead time, cycle time, and throughput were defined in the same run; lead time is queue wait plus
  cycle time, and cycle time is work plus wait.
- At limit 1, cycle time is shortest (4.8 days) and wait share is zero, but lead time is 118.5 days
  and 75% of capacity sits idle — cycle time alone is misleading.
- At limit 4, idle capacity drops to 1%, throughput peaks at 0.83 items/day, and lead time falls to
  17.3 days; up to this point cycle time never degrades.
- Above limit 4, wait share climbs from zero to 84.1%, cycle time stretches to 29.9 days, throughput
  falls to 0.49, and 195.9 person-days go to switching.
- In this run the three metrics point to limits of 1, 4, and 4 respectively; the limit cannot be chosen
  without reading all three together.

## Next Step

Every measurement in this lesson was made on a single team: four people, one backlog, one WIP limit.
Once that assumption is removed, a new cost item is born. When a work item requires a change that
touches more than one team, a **coordination round** has to pass between teams, and that round is
neither work nor queue wait. The next lesson counts the price of multi-team coordination: as the
number of teams grows, how many coordination rounds fall to each dependent work item, what a joint
planning round costs, how much delay the coordination layer adds on its own, and whether this cost
grows linearly or quadratically with team count. The same run asks a second question — how many rounds
are gained by removing which type of dependency.
