Learn
Lesson · Latency and the Tail · The floors

Serial Sums, Parallel Maxes

Stages that run one after another add their times; stages that run beside each other take the maximum. Put both rules together and the end-to-end time is the longest path through the graph of stages. Every stage off that path has slack — room to grow before it changes anything — and the slack column is precisely the list of optimisations that will do nothing.

series t = t₁ + t₂ + … + tₙ parallel t = max(t₁, t₂, …, tₙ) end to end = the longest path through the stage DAG slack = latest start − earliest start; 0 means critical preset: stages total 120 ms, the longest path is 92 ms
Two composition rules, and what they make together

Core ideas

What this lesson establishes

The hard idea is that end-to-end latency is a property of the graph, not of the list of stages. Two systems with identical stage times can differ by thirty percent on how the stages depend on one another.

1

Series adds, parallel takes the maximum

If B cannot start until A finishes, the pair costs t_A + t_B. If they run beside each other and both must finish, the pair costs max(t_A, t_B). Those are the only two rules, and every stage graph is built from them — which is why a fan-out of ten fast calls and one slow one costs what the slow one costs.

2

The end-to-end time is the longest path

Take every chain of stages from the entry to the exit and add the times along it. The request cannot finish before the longest of those chains does, and if nothing else is waiting it does not take longer. That chain is the critical path, and its length is the answer.

3

Slack is the list of optimisations that change nothing

A stage’s slack is how much longer it could take before the end-to-end time moves. A stage with slack can be made instantaneous and the answer stays where it was. On the preset, enrich has 33 ms of slack: taking it from 22 ms to 1 ms leaves the total at 92 ms, exactly.

The material

The longest path, and everything that is not on it

How to evaluate a stage graph by hand, and what the slack column is telling you to leave alone.

The lab’s preset is a request with seven stages. A gateway takes 4 ms and then two things start at once: authz at 18 ms and a cache lookup at 6 ms. The db read at 55 ms needs both authorisation and the cache result; an enrich step of 22 ms needs only the authorisation. render at 12 ms waits for the database and the enrichment, and respond takes 3 ms.

Definition · Critical path

In a graph of stages where an edge means “must finish before”, the critical path is a longest path from an entry stage to an exit stage, measured by the total of the stage times along it. Its length is the end-to-end time. A stage on a critical path has zero slack: any increase in its time increases the end-to-end time by the same amount.

The stage times total 120 ms, and that total is not the answer to anything. Evaluate the graph instead, stage by stage in dependency order, recording for each stage the earliest instant it could start — which is the largest finishing time among the stages it waits for.

stage time earliest start earliest finish gateway 4 0 4 authz 18 4 22 cache 6 4 10 enrich 22 22 44 db 55 22 77 ← waits for authz and cache render 12 77 89 ← waits for db and enrich respond 3 89 92 end to end = 92 ms sum of all stage times = 120 ms critical path: gateway → authz → db → render → respond

The cache lookup finished at 10 ms but the database could not start until 22 ms, so twelve of those milliseconds were spent waiting. That is the cache’s slack. The enrichment finished at 44 ms and render did not start until 77 ms, so the enrichment has 33 ms of slack. Everything else is critical.

  • gateway 4 ms — slack 0, critical
  • authz 18 ms — slack 0, critical
  • cache 6 ms — slack 12 ms
  • enrich 22 ms — slack 33 ms
  • db 55 ms — slack 0, critical
  • render 12 ms — slack 0, critical
  • respond 3 ms — slack 0, critical

Slack, and the trap on the other side of it

Drag the enrich slider from 22 ms down to 1 ms in the lab. The end-to-end time stays at 92 ms, because the enrichment was never what render was waiting for. Now drag respond from 3 ms to 2 ms: the total falls to 91 ms, the whole millisecond, because respond is critical. That is the difference the slack column is describing, and it is the difference between a quarter of work that shows up in the graphs and one that does not.

The trap sits on the other side. Take the database from 55 ms to 45 ms and the total falls from 92 ms to 82 ms — the full 10 ms. Keep going: at 22 ms the total is 59 ms, a saving of 33 ms for a 33 ms improvement. Take it to 10 ms and the total is still 59 ms. Below 22 ms the database has stopped being critical: the path through enrich has taken over, and further work on the database buys nothing. Shortening a critical stage pays in full only until the moment a second path becomes the longest one.

Example · One added dependency, and what it costs

Set the authz → cache selector to present, so the cache lookup waits for authorisation instead of running beside it. Nothing got slower — every stage time is unchanged — but the cache is now on the path and the end-to-end time is 98 ms. Set enrich → db to present instead, so the database needs the enriched key: the total becomes 114 ms and the critical path reroutes through the enrichment. Two edges, twenty-two milliseconds, no code made slower.

The evaluation above works because the stages were listed in an order where every stage came after everything it waits for, so one pass settled each stage’s earliest start in turn. That is a real algorithm with a real proof, and it is not this lesson’s: “Shortest and Longest Paths in DAGs” on the Algorithms path derives the one-pass topological-order method and shows why a single pass is enough. The graph here is small enough to evaluate by reading it, which is the whole reason it is seven stages and not seventy.

The same longest path is a subject in its own right on a third path. “Project Networks and the Critical Path” in Operations Research computes earliest and latest start times, defines slack as the difference, and calls a zero-slack activity critical — the same three columns as the lab, on a schedule of work rather than a request. Its misconception is the complement of this one: there, the error is believing that shortening a critical activity shortens the project by the same amount, which the 55 → 22 → 10 sequence above refutes; here, the error is believing that shortening any stage helps at all.

Interactive

The critical path through a stage graph

Serial stages add, parallel stages take the max, and slack names the rest

Serial sums, parallel maxesthe longest path through the stage graph, and what every other stage has spare
critical pathhas slackdependency

Method

Evaluating a stage graph by hand

Forward once for the answer, backward once for the slack. Both passes are additions.

1

List the stages so that every stage follows what it waits for

Any such order will do and there is usually more than one. With the list in that order, a single forward pass is correct, because everything a stage depends on has already been settled when you reach it.

2

Go forward: earliest start is the largest finish among the predecessors

Entry stages start at zero. For every other stage, take the finishing times of the stages it waits for and keep the largest — this is the max rule — then add its own time to get its finish. This is the parallel rule and the series rule applied one stage at a time.

3

Read the end-to-end time and trace the path back

The answer is the largest finishing time. Walk backwards from it, each time to the predecessor whose finish was the one that set the start. That chain is the critical path.

4

Go backward for slack, and use it as a do-not-touch list

Latest finish for the exit is the end-to-end time; for any other stage it is the earliest of its successors’ latest starts. Slack is latest start minus earliest start. Zero means critical; anything else is a stage whose speed-up will not appear in the total.

Paths and slack

Score 0 / 0

The seven-stage request, forward and backward

The forward pass gives the answer. The backward pass gives the reason four of the seven optimisations available here are worthless.

forward ES = max finish of predecessors, EF = ES + time gateway 4 ES 0 EF 4 authz 18 ES 4 EF 22 cache 6 ES 4 EF 10 enrich 22 ES max(22) = 22 EF 44 db 55 ES max(22, 10) = 22 EF 77 render 12 ES max(77, 44) = 77 EF 89 respond 3 ES 89 EF 92 end to end 92 ms backward LF = min start of successors, LS = LF − time respond LF 92 LS 89 slack 89 − 89 = 0 critical render LF 89 LS 77 slack 77 − 77 = 0 critical db LF 77 LS 22 slack 22 − 22 = 0 critical enrich LF 77 LS 55 slack 55 − 22 = 33 cache LF 22 LS 16 slack 16 − 4 = 12 authz LF 22 LS 4 slack 4 − 4 = 0 critical gateway LF 4 LS 0 slack 0 − 0 = 0 critical

Two stages have slack and five do not, so five of the seven possible speed-ups are worth something and two are worth nothing at all. That is a decision made by arithmetic in under a minute, and it is the decision a profiler will not make for you: a profiler ranks stages by how long they take, and enrich at 22 ms is the second-largest stage here and the least useful of all seven to improve.

The slack numbers are also a budget. The cache lookup can grow from 6 ms to 18 ms before anything changes, and the enrichment from 22 ms to 55 ms. If a proposed change makes the enrichment twice as expensive but removes a database query, the slack column says the first half of that trade is free.

For a faded rehearsal, set the enrich → db selector to present, so the database waits for the enrichment. The supplied first move is the new earliest start for db, which is now max(22, 44) = 44. Carry the forward pass through to respond, name the new critical path, and say which stage has just acquired slack and how much. Then check the four figures the lab prints against yours before opening the quiz.

Common mistakes

1
Adding the stage timesThe preset’s seven stages total 120 ms and the request takes 92 ms. The sum is an upper bound that is reached only when nothing runs in parallel, and quoting it as the latency overstates by whatever the parallelism was worth — here, thirty percent.
2
Optimising the largest stage rather than a critical oneSize and criticality are different properties. enrich at 22 ms is the second-largest stage on the preset and contributes nothing; respond at 3 ms is the smallest and every millisecond of it is real. Rank by slack, not by duration.
3
Adding a dependency without noticing it is a latency changeMaking the cache lookup wait for authorisation costs 6 ms of end-to-end time while making no individual stage slower. Changes like this arrive as refactors, correctness fixes or simplifications, and they never appear in a profile as a regression in any one stage.

Check your understanding

✓
Finish when you look for the path before you look for the biggest number.

You should be able to evaluate a stage graph forward for the end-to-end time, name the critical path, compute the slack on every other stage, and predict both what a speed-up on a given stage buys and the point at which it stops buying anything.

Note: Every stage time so far has been a single number, as if a stage always took exactly that long. It does not: it has a distribution, and the interesting part of that distribution is its tail. “Percentiles from a Sample” makes the tail into an object you can compute with, and “Percentiles Do Not Add” returns to this lesson’s series rule to show that it survives for means and fails for percentiles.

Kept in this browser only, for you. Nothing is sent anywhere and nothing checks it. Carry it to another device.

Feedback and recommendations

Something to change about this lesson? Write it down, tick it off when it is handled, and export the list when you want to act on it. Kept in this browser only: nothing is sent anywhere, and nobody is notified.

No recommendations yet for this lesson.