Software Engineering · Engineering Process
Critical Path, Float, and Why the Longest Chain Sets the Date
How the forward and backward passes compute earliest and latest dates, what total float measures, and why a zero-float path is not necessarily the riskiest one.
A plan expressed as a set of activities with durations and dependencies has a property that no amount of effort changes: the completion date is the length of the longest path through the graph, not the sum of the work. Forty person-days of effort spread across six independent tasks can finish in twelve days if the chains are short and the people are available; twelve person-days concentrated on one chain will take twelve days even with half the team idle. Critical path methods, developed for the Polaris programme in the late 1950s, make that distinction computable.
Two passes over an acyclic graph
The model is a directed acyclic graph in which nodes are activities with durations and edges are finish-to-start dependencies. Two traversals produce everything else.
The forward pass computes early dates. An activity may start at the latest early finish among its predecessors, and its early finish is its early start plus its duration. Activities with no predecessors start at zero. Taking the maximum, rather than the sum, is the entire insight: parallel work overlaps.
The backward pass computes late dates from an imposed completion date, or from the end of the graph. An activity’s late finish is the earliest late start among its successors, and its late start is its late finish minus its duration.
The difference between the late and early dates is float, the amount an activity may slip without moving the project completion date. Total float is defined per activity: late start minus early start. Free float is narrower, the amount an activity may slip without moving any successor’s early start, and it equals the smallest early start among successors minus the activity’s own early finish.
tasks = { # name: (duration_days, [predecessors])
'spec': (2, []),
'api': (5, ['spec']),
'schema': (3, ['spec']),
'ui': (4, ['api']),
'migrate': (5, ['schema']),
'release': (1, ['ui', 'migrate']),
}
order, indeg = [], {n: len(p) for n, (_, p) in tasks.items()}
ready = [n for n, d in indeg.items() if d == 0]
while ready: # Kahn's algorithm, deterministic order
n = ready.pop(0)
order.append(n)
for m, (_, preds) in tasks.items():
if n in preds:
indeg[m] -= 1
if indeg[m] == 0:
ready.append(m)
es, ef = {}, {}
for n in order:
es[n] = max((ef[p] for p in tasks[n][1]), default=0)
ef[n] = es[n] + tasks[n][0]
duration = max(ef.values())
ls, lf = {}, {}
for n in reversed(order):
succ = [m for m, (_, preds) in tasks.items() if n in preds]
lf[n] = min((ls[m] for m in succ), default=duration)
ls[n] = lf[n] - tasks[n][0]
print({n: ls[n] - es[n] for n in order}) # total float per activity
In the example the chain through spec, api, and ui totals eleven days, while the chain through schema and migrate totals ten, so the release sits at day twelve and migrate holds one day of float while the chain above it holds none.
What float is for
Float is a budget for uncertainty, allocated per activity by the structure of the graph. An activity with five days of float can absorb five days of trouble before the end date moves. An activity with zero float moves the end date on the first day of slippage, which is what being on the critical path means.
The budget is not transferable in the way schedules assume. Float belongs to the path, not to the activity: if one activity on a zero-float path slips by three days, every other activity on that path now has negative float relative to the original completion date, and the whole chain has to be re-planned or the date has to move. Schedules that report float per task obscure this, because the numbers on the report look like slack that each owner may spend independently.
Negative float deserves attention rather than repair. It appears when a late finish is imposed by something other than a successor, typically a fixed date from outside the graph. A task showing minus four days of float is a statement that the imposed date is infeasible given the durations and links, and it is the only signal in the model that distinguishes “we are behind” from “we never had room”.
Float gets consumed, and by whom
In any schedule with slack, the slack is spent. The mechanism is not laziness so much as the absence of an owner: an activity with float has no visible consequence for finishing late, and its participants have competing demands that do have consequences. Work expands to the time available, and review latencies expand to fill the gap before the next thing starts. By the time the slack is needed, it is gone, and the previously non-critical path becomes critical without any single decision to that effect.
Two structural remedies exist. Buffer management concentrates the float at the end of a chain instead of distributing it per task, so slippage anywhere in the chain consumes a shared, visible reserve, and the rate of consumption is a leading indicator. Alternatively, chains are shortened so that float is unnecessary: fewer dependencies, smaller items, and parallel work that does not converge on one person. The work-in-progress discussion covers why the second option often fails when a shared reviewer sits at the convergence point.
The zero-float path is not the risk path
Critical path analysis as classically defined uses single-point durations, typically the mean or the most likely. That produces the path with the largest expected length, which is not the path most likely to determine the actual date.
Consider two chains of equal expected length: one is a single activity with a tight range, the other is six activities each with a wide range. The second chain has far more chances to run late, and its maximum is what the project waits for. The distribution of a path’s length is the convolution of its activities’ distributions, and a path with a slightly smaller mean but a much larger variance will exceed the nominal critical path often enough to dominate the outcome. PERT addressed this by treating activity durations as random and computing the probability that each path finishes by a target date, at the cost of assuming independence between activities, an assumption that fails exactly when a shared unknown drives several of them.
The computation is cheap once the graph exists. Ranking activities by total float gives the near-critical set directly, and the size of that set relative to the whole plan is a useful measure of how much reserve a schedule holds: a plan whose activities almost all carry zero float has no reserve anywhere, so every risk lands on the date. Free float, being smaller or equal to total float, identifies the activities whose slippage immediately delays someone else’s start, which are the ones worth watching for coordination rather than for the end date.
A practical consequence is that the critical path should be recomputed after any significant change, and that paths within a few percent of the longest one should be tracked as well. The threshold is a judgment call, but the failure mode of ignoring near-critical paths is well defined: the plan protects a chain that is not the constraint while the real constraint slips unobserved.
Resource constraints leave the model behind
Everything above assumes unlimited resources. Once two activities that both need the same specialist are linked by that shared capacity, the graph changes: the link is no longer a stated dependency but a scheduling decision, and deciding the order is part of the problem. Resource-constrained project scheduling is NP-hard, so practical tools use heuristics such as priority rules and serial schedule generation, which place activities one at a time in a topological order, delaying any whose resources are unavailable.
The heuristic nature has an observable consequence: two tools given the same activities, durations, and resource pool can produce different dates, both defensible, because the search for the optimum is not exhausted. Where the schedule is resource-constrained, the binding constraint is often a person rather than a dependency, and the critical path reported by the tool is really the critical sequence the heuristic happened to choose. Sequencing work by cost of delay treats that order-sensitivity directly, since the ordering rule determines which of several feasible sequences is preferred.
Lags are the last modelling trap. A ten-day review that involves no work is a lag on an edge, not an activity, and lags hide waiting time inside the graph where nobody sees it as work in progress. Modelling the review as an activity with a duration and a capacity makes the queue visible, which is usually the point: an edge with a lag can be infinite and no report will ever show it.
References
- Critical-Path Planning and Scheduling — J. E. Kelley and M. R. Walker, Proceedings of the Eastern Joint Computer Conference, 1959
- Application of a Technique for Research and Development Program Evaluation — D. G. Malcolm, J. H. Roseboom, C. E. Clark, and W. Fazar, Operations Research, 1959
- Computers and Intractability: A Guide to the Theory of NP-Completeness — Michael R. Garey and David S. Johnson, W. H. Freeman, 1979