Cyclomatic Complexity: A Rollout Risk Signal, Not a Gate
Summary
Cyclomatic complexity usually gets treated as a static CI gate: cap it at 10, fail the build, move on. That misses the number that actually predicts rollout risk, the complexity delta a single diff introduces, checked against whether its tests grew to match. This piece breaks down why a flat complexity score hides bumpy-road functions, why AI-authored diffs make that worse, and how to gate canary pacing on complexity delta instead of waiting for the SLO to catch the damage.
Cyclomatic complexity counts the number of independent paths through a function. NIST puts the safe ceiling at 10 per function, and most teams enforce something close to that in CI. What that number does not tell you is which of those paths will actually get exercised by the canary you're about to ship to 5% of production traffic, and that gap is where most rollout incidents actually start. Treat cyclomatic complexity as a rollout risk input, not a lint rule, and the number starts earning its keep.
What Cyclomatic Complexity Actually Measures
McCabe's metric is a graph count: nodes, edges, connected components. Every if, for, while, case, and boolean operator adds a path. A function with a complexity of 25 has at least 25 linearly independent paths through it. That's not opinion, it's basis path testing math, and it sets a floor on how many test cases you'd need to cover the function once, end to end. Sourcegraph's breakdown has the full thresholds: 1-10 low risk, 11-20 moderate, 21-50 high, 50+ is the "we should talk" tier.
Most teams don't hit that floor. Coverage tools report line coverage, not path coverage, so a function can show 90% coverage while half its branches never fire in CI. That's the part nobody puts on the PR template, and it's the reason "tests pass" and "this is safe to ship" are two different claims that get treated as one.
We've reviewed enough post-mortems to notice the pattern: the incident review always has a line for error budget burn and a line for time to detect. It almost never has a line for what the complexity of the changed function looked like before the diff landed. That's a gap in the paperwork, not just the tooling.

Why a Flat Complexity Number Hides the Real Blast Radius
Here's the part every complexity dashboard gets wrong: two functions can post the identical cyclomatic complexity score and carry wildly different risk. A 20-branch switch statement dispatching to well-tested handlers is not the same animal as a function with four separate pockets of nested conditionals scattered across 80 lines, each one three levels deep. CodeScene calls the second shape a "bumpy road," and the name is accurate. It's the nesting depth, not the raw branch count, that taxes working memory and hides the edge case nobody thought to test. CodeScene's writeup walks through the comparison in detail, and it maps cleanly onto what we see in rollout data.
This matters for us specifically because a canary doesn't fail on average complexity. It fails on the one bumpy function that got touched in this diff, at 2am, under load nobody load-tested. In the post-mortems we've reviewed with platform teams, the pattern repeats: the root-cause function almost never has the highest complexity score in the repo. It has the highest complexity delta in the diff that shipped. That's a different signal, and almost nobody instruments it.
A dashboard that tells you the codebase's complexity is trending down is not the same dashboard that tells you this rollout, right now, touched a function that just picked up three new nested branches. One is a health report you read once a quarter. The other is a gate you'd actually want in the deploy pipeline, sitting next to the SLO check, not buried in a separate static-analysis tool nobody opens during an incident.
Ring-based and percentage-based rollouts already assume some diffs are riskier than others; that's the entire premise of a canary. What most pipelines don't do is let the complexity shape of the diff itself inform how fast that canary should widen. It's treated as a code-review concern, then forgotten the moment the PR merges.
The CI Gate Everyone Sets at 10, and Why It Doesn't Move the Needle
The standard advice, cap cyclomatic complexity at 10 and fail the build above that, is the "skip" step nearly every engineering blog recommends and almost nobody validates against actual incident data. It's not wrong, exactly. It's incomplete in a way that lets teams believe they've handled the risk when they've mostly just moved it.
Two failure modes, both common on teams we've talked to:
Gaming the number. Extract Method is the textbook fix, and it works: cyclomatic complexity per function drops. But if the extraction doesn't reduce the actual decision count, just relocates it across three functions instead of one, the system's complexity is unchanged. The gate goes green. The blast radius doesn't shrink, it just gets harder to see in a single diff view.
Ignoring the shape. A flat threshold treats a 12-branch dispatcher and a 12-path bumpy road as equally risky. They're not. The dispatcher is probably fine; it's mechanical routing. The bumpy road is where your next rollback lives, because the person reviewing it stopped tracking state three nested levels in.
AI coding agents make this worse before they make it better. Devin and similar autonomous agents ship PRs fast, and "fast" often means adding a branch instead of refactoring the one that's already there. That's the path of least resistance for a model optimizing for a passing test suite, not for a reviewer's cognitive load. If your team is merging AI-authored diffs at volume, complexity delta per PR is a metric you want on the dashboard before it becomes an incident review finding, not after.

What the Complexity Delta Actually Tells You About Testing Burden
Forget the absolute score for a second. The number that predicts incident risk is the change in complexity introduced by a single diff, cross-referenced against whether the tests touching that diff actually grew to match it.
A function that goes from complexity 8 to complexity 19 in one PR has, by the basis-path-testing math above, roughly doubled its minimum required test count. If the PR added two tests, you've shipped a coverage gap disguised as a passing CI run. That gap doesn't show up until the canary hits the 5% traffic slice that exercises the untested branch, and by then it's an incident, not a code review comment sitting unresolved in a PR thread.
This is the instrumentation gap upstreamapi's AI Pilot is built to close on the rollout side: gate the canary percentage and hold time on the complexity delta of the shipped diff, cross-checked against its test delta, not just on the downstream error-rate SLO. An error-rate SLO tells you something already broke. A complexity-delta gate tells you the diff was more likely to break something before it's serving live traffic, which is the only point where that information is still actionable.
// Simplified canary gate: widen slowly on low-risk diffs, hold on high-risk ones
function canaryStep(diff: DiffMetrics): CanaryDecision {
const complexityRisk = diff.complexityDelta / Math.max(diff.testDelta, 1);
if (complexityRisk > 3 && diff.slo.errorBudgetBurn > 0.1) {
return { action: "hold", trafficPct: diff.currentTrafficPct };
}
if (complexityRisk > 3) {
return { action: "extend_bake_time", bakeMinutes: 45 };
}
return { action: "advance", trafficPct: diff.currentTrafficPct + 10 };
}Does the Research Even Agree on This?
No, and that's worth saying plainly. Some practitioner research pushes back hard on cyclomatic complexity as a defect predictor at all, arguing teams that optimize for a lower score often just relocate complexity somewhere less visible. GetDX's critique makes this case and points teams toward developer-experience metrics instead. We don't think that argument kills the metric; we think it kills the metric used alone, as a static repo-wide score, disconnected from the diff and the rollout it's attached to.
How We'd Actually Gate a Canary on Complexity, Not Just Error Rate
Three things, in order of how much friction they add to a PR:
Compute complexity delta per diff, not per repo. Repo-wide averages hide the one function that matters this week. Delta per PR is cheap to compute in most static analysis tools, and it's the number that correlates with what actually breaks in the following 48 hours.
Cross-reference against test delta, not test count. A function with 40 tests and a complexity jump from 8 to 19 with zero new tests is a bigger risk than a fresh function with complexity 15 and matching coverage from day one.
Feed the ratio into rollout pacing, not just merge approval. A high complexity-delta diff doesn't need to be blocked at review; plenty of legitimately complex domain logic (state machines, protocol parsers) will always score high. It needs a slower canary and a shorter error-budget leash, which is a rollout decision, not a code review decision.
Write the runbook entry before the rollout, not after the page. If the on-call engineer opening the incident channel at 3am has to reverse-engineer why a "clean" PR just burned the error budget, the documentation failed before the code did. A one-line note in the deploy log ("complexity delta 11, test delta 1, held at 20%") costs nothing to write and saves the first fifteen minutes of every incident review that follows.

Is It Worth Tracking, or Just Another Dashboard Number?
Depends entirely on where you attach it. Cyclomatic complexity as a repo-health trend line is mostly decoration: nice for a quarterly slide, useless at 2am. Cyclomatic complexity as a per-diff delta, fed into canary pacing and cross-checked against test growth, is one of the cheaper leading indicators we've found for "this rollout is going to page someone."
The post-mortem will ask what the SLO budget looked like before the rollback. Increasingly, ours also asks what the complexity delta looked like on the diff that shipped. Worth adding that question to your own runbook before the incident forces the conversation, not during the retro when the answer is a shrug and a promise to "add better tests next time."