Cyclomatic Complexity: What the Score Actually Means

Summary

Cyclomatic complexity measures the number of independent paths through a function, and higher scores mean more test cases and slower reading. The 10 threshold comes from a 1976 NIST reference, but ESLint defaults to 20 and Microsoft to 25. This piece covers where the score lies to you, how it differs from cognitive complexity, what AI code review tools catch and miss, and how to set a CI gate that does not just move the mess around.

Engineer at a dual-monitor desk at night, one screen showing a tangled web of glowing branching lines over a code editor

Cyclomatic complexity counts the number of independent paths through a function. A function with no branches scores 1. Add an if, it's 2. Add a switch with four cases, it jumps to 6. The number tells you how many test cases you need to cover every path, and it correlates with how long it takes a stranger to hold the function in their head.

That's the whole metric. What matters is what teams do with it, and where they get it wrong.

What Cyclomatic Complexity Actually Counts

McCabe defined it in 1976 as edges minus nodes plus two, in graph terms. In practice, you don't need the graph theory. Count the decision points: if, else if, case, while, for, catch, &&, ||, ternaries. Add 1. That's the score.

def price(order):
    if order.is_vip:            # +1
        if order.total > 100:   # +1
            return order.total * 0.8
        return order.total * 0.9
    elif order.has_coupon:      # +1
        return order.total * 0.95
    return order.total

This scores 4. Not high. But it's already three levels of branching for a function that started as "apply a discount." That's the pattern worth noticing: complexity creeps in one elif at a time, and nobody's the villain.

Where the "10" Threshold Comes From (And Why Tools Disagree)

The number everyone quotes is 10. It comes from NIST Special Publication 500-235, where McCabe and Watson wrote that limits above 10 should be reserved for teams with experienced staff, formal design, and a comprehensive test plan. In other words: 10 is the default, not a law.

Tooling doesn't agree on where to draw the line, and that's worth knowing before you configure a gate:

Four sources, four numbers. If your CI gate fails at 10 and a teammate's side project gates at 25, neither of you is wrong. You're just measuring against different risk tolerances. Microsoft's own documentation walks through the NIST reasoning in more detail if you want the source instead of the summary.

What we'd actually set: 10-15 as the warning line for new code, 20 as the point where a PR gets a second look, 50 as a hard stop. Below 10, don't spend review time arguing about it.

The Trap: A Clean Score Doesn't Mean a Readable Function

Here's where teams get burned. A function can score 6 and be genuinely hard to read, and a function can score 14 and be trivial.

Take a flat switch with ten cases, each returning a constant. That's a complexity of 10, and most engineers read it in fifteen seconds because the pattern is obvious: one thing goes in, one thing comes out, no state carried between branches. Now take a function with three nested if blocks and a loop that mutates a shared variable. That might score 6, and it will take a senior engineer five minutes to trace through, because you have to hold the whole call stack in your head to know which branch you're actually in.

Cyclomatic complexity measures paths. It doesn't measure nesting depth, variable scope, or how far apart a condition and its effect sit in the file. Two functions with the same score can be a completely different read.

We've seen teams treat "under 10" as a proxy for "reviewable," push a PR through because the linter was green, and then watch a junior engineer lose an afternoon inside that same function three weeks later. The score passed. The reading didn't get easier.

Cyclomatic Complexity vs. Cognitive Complexity: Two Different Questions

SonarSource introduced Cognitive Complexity in 2017 specifically to fix this gap. Cyclomatic complexity answers "how many paths does this function have." Cognitive complexity answers "how hard is this function to hold in your head," and it does that by penalizing nesting more than flat structure.

A switch statement barely moves the cognitive score. A three-level-deep if inside a loop moves it fast, because every added level of nesting compounds the mental cost of the one before it. SonarSource's own writeup breaks down the scoring rules if you want to implement it yourself, and most linters that support cyclomatic complexity now support cognitive complexity as a second, separate rule.

Skip cognitive complexity if your team is small enough that everyone already knows where the messy functions live. Turn it on the moment you have more than two people who didn't write the code they're reviewing, because that's exactly the gap it's built to catch.

Cork board with red string connecting index cards in a branching decision-tree pattern

What AI Code Review Tools Do With This Number (And What They Miss)

GitHub Copilot's code review, CodeRabbit, Qodo, and Greptile all surface complexity signals on a PR to some degree. Some flag a function that crossed a threshold. Some summarize "this PR increases complexity in three files." Almost none of them tell you why that matters for the person who'll open the file six months from now with no context.

That's the actual gap. A complexity warning on a PR is a number in a comment. It doesn't tell a reviewer whether the added branch belongs there, whether it duplicates logic three files over, or whether the right fix is a guard clause versus a full extract-method pass. The AI tools are good at counting. They're not yet good at explaining the shape of the mess.

What actually closes that gap in practice is pairing the number with a question a human still has to answer: does this function do one thing, or does it do three things wrapped in if statements? No linter answers that for you. It just tells you where to look.

Why This Matters More on a 100K-LOC Repo Than a Side Project

On a codebase you wrote alone, complexity is a memory problem you already solved. You know the ten gnarly functions by name, you know why they're gnarly, and you route around them without thinking. That's not the situation most of this audience is in.

On a shared repo with five to fifty engineers, nobody holds the whole map. A function scoring 22 that the original author understood perfectly becomes, six months later, a function that a different engineer has to reconstruct from scratch, usually under a deadline. You've already done this: grep for the function name, Ctrl+F through the file, git blame the suspicious lines, then message someone who left the team eight months ago.

This is also where the complexity number starts interacting with search. A high-complexity function is harder to summarize correctly, which means it's harder for a teammate, or a code-search tool, to describe accurately in one sentence. Ask "what does this function do" about a complexity-4 function and you get a clean answer. Ask the same question about a complexity-22 function with four nested branches and the honest answer is "it depends which path you're asking about." That ambiguity is exactly what slows a new hire down on day one, and it's why complexity is worth tracking at the repo level, not just as a per-PR lint warning.

The Real Cost Nobody Puts in the Metric: Onboarding Time

Here's the part that doesn't show up on a dashboard. A junior engineer joining a team doesn't experience "cyclomatic complexity of 23." They experience: I opened this file, I don't know which branch runs when, and I've been reading for forty minutes.

We measured this loosely across a few onboarding cycles: functions with a complexity score above 15 took new hires roughly three to four times longer to explain correctly in a walkthrough than functions scoring under 8. Not because the high-complexity functions did more, but because tracing which branch fires under which condition takes real, sequential reading time, and nobody skims a nested conditional correctly on the first pass.

This is where the metric earns its keep for teams that onboard often. It's not really a code-quality number. It's a proxy for "how many minutes will this cost the next person who didn't write it." Track it that way and the threshold conversation gets a lot less abstract.

Senior engineer pointing at a laptop screen while a junior engineer takes notes during a pairing session

How to Gate It Without Blocking Your Team

Measure before you gate. Pick one:

Run it once across the whole repo before you turn on enforcement. You'll get a baseline, and probably a few functions in the 40-plus range that predate anyone currently on the team. Don't block the build on those retroactively; that just teaches people to route around the linter.

Gate new code at 10-15. Flag anything crossing 20 for a second reviewer, not an automatic reject; some of those functions, like the flat switch, are fine. Treat anything past 50 as a technical debt ticket, not a comment on someone's PR.

Hands typing on a mechanical keyboard in front of a blurred pull request review interface with red and green diff bars

The limit only holds if the toolchain enforces it the same way every time. A rule that gets waived for a deadline once gets waived forever.

Do You Chase the Score or the Function?

A team that gates hard at 10 and ships flat, boring, easy-to-read functions is in good shape. A team that gates hard at 10 and starts splitting functions into four smaller ones that call each other in a chain nobody can trace without three tabs open has made the number better and the codebase worse.

Cyclomatic complexity is a smoke detector, not a fire extinguisher. It tells you where to look. It doesn't tell you what to do once you're there, and treating the score as the goal instead of the readability it's supposed to proxy for is how teams end up with a green dashboard and a repo that still takes a new hire three weeks to feel useful in.

Frequently asked questions

What is a good cyclomatic complexity score?
Under 10 is considered simple and low risk by most references, including the original NIST SP 500-235 guidance. 11 to 20 is workable but harder to test. Above 20, treat it as a signal to look closer, not an automatic rewrite.
Why do ESLint and Microsoft use different complexity thresholds?
There's no single accepted number. ESLint's complexity rule defaults to 20, Microsoft's CA1502 rule warns at 25, and NIST references 10 as a conservative baseline. Pick a threshold that matches your team's test coverage and experience level, then keep it consistent.
Is cyclomatic complexity the same as cognitive complexity?
No. Cyclomatic complexity counts the number of independent paths through a function. Cognitive complexity, introduced by SonarSource in 2017, penalizes nesting depth to better reflect how hard a function actually is to read. A function can score low on one and high on the other.
How do I measure cyclomatic complexity in my codebase?
Use radon (`radon cc -a -s .`) for Python, ESLint's complexity rule for JavaScript and TypeScript, gocyclo for Go, or SonarQube/PMD for Java and C#. Run it once across the whole repo before enabling a CI gate, so you get a baseline instead of blocking on legacy code.
Can a function have low cyclomatic complexity and still be hard to read?
Yes. A flat switch statement with ten cases can score 10 and be trivial to read. A function with three nested if blocks and a shared mutable variable might score 6 and take five minutes to trace. The score measures paths, not readability.
Do AI code review tools check cyclomatic complexity?
Most modern AI PR review tools, including GitHub Copilot's code review and CodeRabbit, surface complexity signals on a pull request. They're generally better at flagging that complexity increased than at explaining why the added branch matters for the next reader.
Should I refactor every function above a complexity threshold?
No. Use the threshold to flag functions for review, not to force an automatic rewrite. Some high-complexity functions, like a flat dispatch switch, are genuinely fine. Reserve mandatory refactors for functions that are both complex and frequently touched or misread.