Skip to content
Kumar Chandrachooda
Engineering Practice

Cyclomatic Counts Paths, Cognitive Counts Pain

Two complexity metrics that look interchangeable and answer different questions - McCabe's 1976 path count is a testing instrument, SonarSource's cognitive score is a readability instrument, and only one of them earns a place in the merge gate.

By Kumar Chandrachooda 23 Apr 2026 7 min read
Two gauges reading the same function and disagreeing about it

Two functions arrive in the same pull request. The first is a routing table wearing code — a flat switch over seven notification kinds, one return per case. The second is a loyalty-discount check four conditionals deep. Ask cyclomatic complexity which one deserves scrutiny and it points, with a straight face, at the routing table. Anyone who has read both knows that is backwards, and the gap between what the metric says and what your eyes say is what this part is about. Part 1 set the rules of engagement — know each instrument well enough to read it without being played by it — and complexity is where the knowing starts, because the two headline complexity metrics look interchangeable, get used interchangeably, and answer different questions.

The path counter from 1976

Cyclomatic complexity is the older instrument by a comfortable margin. Thomas J. McCabe introduced it in 1976, in a paper titled “A Complexity Measure” in the IEEE Transactions on Software Engineering, and the definition has not moved since: the number of linearly independent paths through a function's control-flow graph. In practice you count it without drawing any graph — start at one and add one for every decision the code makes: each if, each loop, each case label, each catch, each short-circuiting boolean operator. The result is a whole number with a genuinely useful meaning, because it is the size of a basis set of paths through the function — a practical floor on the number of test cases you need before every branch has been exercised. That is what the metric was for. McCabe was writing about testing, and as a testing measure the count is as honest today as it was five decades ago.

The traffic-light banding that now travels with it — 1 to 10 simple, 11 to 20 moderate, 21 to 50 high risk, above 50 untestable — deserves a footnote it rarely gets. Those bands are convention, popularised across decades of risk guidance (the Software Engineering Institute did much of the popularising); McCabe published the number, not the thresholds. The count is mathematics. The colours are folklore that hardened.

One metric, two very different functions

Here is the routing table from the pull request, freshly invented for this article but shaped like a hundred functions you have merged without a second look:

public string ChannelFor(NotificationKind kind)
{
    switch (kind)
    {
        case NotificationKind.OrderConfirmed:  return "email";
        case NotificationKind.OrderDispatched: return "email";
        case NotificationKind.DeliveryWindow:  return "sms";
        case NotificationKind.DeliveryFailed:  return "sms";
        case NotificationKind.PaymentDeclined: return "email";
        case NotificationKind.RefundIssued:    return "email";
        case NotificationKind.PriceDrop:       return "push";
        default:                               return "none";
    }
}

Seven case labels, so cyclomatic complexity lands at eight. And here is the discount check:

public bool QualifiesForLoyaltyDiscount(Customer customer, Basket basket)
{
    if (customer.IsRegistered)
    {
        if (customer.YearsActive >= 2 || customer.LifetimeSpend > 1000m)
        {
            if (basket.Total >= 40m)
            {
                foreach (var line in basket.Lines)
                {
                    if (line.IsClearance)
                        return false;
                }
                return true;
            }
        }
    }
    return false;
}

Six decisions plus the starting one: cyclomatic complexity of seven. By the path count, the switch is the more complex of the two — and to be fair to McCabe, for the purpose he built the metric that answer is correct. The routing table genuinely needs eight test cases to one per branch; the discount check needs seven. If you are planning tests, the instrument is telling you the truth.

But nobody dreads maintaining the routing table. You read it the way you read a menu, top to bottom, no state to carry. The discount check is the one that will eat a reviewer's afternoon: to understand the innermost return false you are holding four accumulated conditions in your head, one of which is itself a disjunction. Cyclomatic complexity treats every decision as equally expensive, and human working memory emphatically does not. A metric that scores the menu above the maze is measuring something real — it just is not measuring difficulty.

The metric that charges rent for nesting

That mismatch is precisely the gap cognitive complexity was built to close. Published by G. Ann Campbell at SonarSource in a freely available white paper, it positions itself as the successor to cyclomatic complexity for one specific question — and it was designed and validated to track how hard code is to understand. Not defect counts, not test counts: understanding effort. It is a readability instrument, and judged as one it is the best widely implemented option we have.

The mechanics differ from McCabe's in two moves. First, structures that read linearly are cheap or free: a switch, however many cases it carries, costs a single increment, because a reader processes it as one decision with a list of answers. Second, and more importantly, nesting is charged rent. Each break in linear flow costs one increment plus one more for every level of nesting it sits inside, which encodes the lived truth that a conditional inside a loop inside a conditional is far harder to reason about than three conditionals in a row.

Run those rules over the two functions above. The routing table scores one — a single increment for the switch, nothing per case. The discount check scores sixteen: one for the outer if, two for the second (its increment plus a nesting penalty), one for the ||, three for the third if, four for the loop, five for the innermost check. Eight versus seven under McCabe becomes one versus sixteen under Campbell, and the second ranking is the one that matches your experience of reading them. Sixteen also happens to cross the line that matters in practice: SonarSource's documented default threshold for its cognitive-complexity rule is fifteen per function, and that is the number I recommend leaving exactly where the vendor put it.

The honest caveat: cognitive complexity is still a structural proxy. It cannot see that a variable is misleadingly named, that the domain itself is intricate, or that a well-chosen abstraction would collapse the whole function into two lines. Code can score five and still be wrong, or score twenty and be the clearest available expression of a genuinely branchy business rule. It measures the shape of the control flow, not the quality of the thinking — no complexity metric does better, and it is worth remembering that none claims to.

Retiring Halstead fairly

There is a third name that belongs in any honest complexity survey, if only to explain why you will not meet it again in this series. Maurice Halstead's metrics, introduced in 1977 in Elements of Software Science, take a radically different approach: count the distinct operators and operands in a program, then derive a family of measures from the counts — Volume for information content, Difficulty for error-proneness, Effort for the mental cost of writing or understanding the thing.

It was a serious and pioneering attempt to make software measurable, and it deserves better than the sneer it usually gets. But as a standalone instrument it has aged out. The operator-versus-operand distinction, crisp enough in the Fortran era, turns ambiguous in modern languages — is a lambda an operator, an operand, or both — and no agreed thresholds ever emerged, so the numbers can only be compared against themselves over time. Where Halstead survives is inside a composite: his Volume is one third of the maintainability index, which means you have been consuming Halstead maths every time an IDE shows you a green maintainability score. Retired as a soloist, still employed in the orchestra — and the orchestra is the next part's subject.

Which one gates my merges

Here is how the two survivors divide the work in my own practice, offered as tested defaults rather than industry law. Cognitive complexity above fifteen per function blocks the merge; cyclomatic complexity gates nothing and plans tests. The blocking gate points at cognitive because the failure it guards against is a human one — the function nobody can safely modify in eighteen months — and cognitive is the instrument validated against that exact cost. Cyclomatic stays on the dashboard because when I review a function's tests, its path count tells me immediately whether the suite can possibly be adequate: seven paths and three tests is a conversation, whatever the coverage percentage claims.

And the part 1 caveat applies with full force, because complexity gates are prime theatre material. Point the threshold at the code and it improves the codebase; point it at the author and you get the failure the opener described — functions split into trivially decomposed fragments that each duck under the limit while the complexity migrates into a call graph no metric is watching. The gate should read, “this function costs too much to understand — spend the cost now, while you still remember what it does”, and never “this author writes bad code”. The moment it becomes the second sentence, expect creative compliance within the sprint.

Both of these metrics live entirely inside a single function, which makes them necessary and nowhere near sufficient. The obvious next want is one number for a whole module's health — and there is a famous formula that promises exactly that, with Halstead's Volume baked inside it. What it actually delivers is the subject of the next part: the maintainability index and the debt ratio.