# Cyclomatic Complexity: What the Score Actually Tells You

URL: https://desktoptools.net/journal/cyclomatic-complexity
Type: blog
Locale: en
Published: 2026-07-31
Updated: 2026-08-09

---

> What cyclomatic complexity actually measures, why the flat threshold of 10 misleads teams, and which tools (SonarQube, CodeScene, Understand, Code Climate) get you the real number.

Cyclomatic complexity counts the number of independent paths through a function: every `if`, `for`, `while`, `case`, and `catch` adds one branch to the total. A function with a score of 1 has no branching at all, one straight line from entry to return. A function with a score of 25 has 25 distinct routes execution can take, and 25 distinct routes a test suite would need to cover to call itself complete. NIST puts the ceiling at 10 per function. Most teams never check.

## How the Number Gets Calculated

The formal definition is M = E − N + 2P, where E is edges in the control-flow graph, N is nodes, and P is connected components. For a single function that simplifies to M = E − N + 2, which is a graph-theory way of saying: count the decision points, add one.

You rarely need the formula by hand. Any linter or static analyzer walks the AST, counts `if`/`else if`, `while`, `for`, `case`, `catch`, and boolean operators like `&&` and `||` inside conditionals, and adds 1 for the function's single entry point. The boolean operators matter more than people expect: `if (a && b)` is not one decision, it's two, because `a` and `b` are each independently capable of sending execution down a different path. A ten-line function with three nested conditionals and a couple of `&&` chains can hit 6 or 7 before you've written a single loop.

`// cyclomatic complexity: 7 (four if-branches + two boolean operators)
function classify(user) {
  if (user.ageGroup === 'child') return 'child';
  if (user.ageGroup === 'teen' && user.hasParentalConsent) return 'teen';
  if (user.region === 'EU' || user.region === 'UK') return 'gdpr';
  if (user.isBanned) return 'blocked';
  return 'standard';
}`That function reads fine on a screen. It still needs 7 test cases to claim full path coverage, and most test suites stop at 2 or 3: the happy path and maybe one edge case. The gap between "looks readable" and "is actually tested" is exactly what the number is measuring.

![Overhead flat-lay of a developer desk with a laptop terminal, a notebook sketch of branching arrows, and a coffee mug](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/desktoptools/2026-08/fbed5d-inline1.webp)

## What Counts as Too Complex

The industry has more or less converged on four bands. 1 to 10 is low risk: easy to test, easy to hold in your head while reading a diff. 11 to 20 is moderate: still testable, but the branch count means your suite needs real coverage, not a happy-path check. 21 to 50 is high: hard to test thoroughly, and a strong candidate for a split before the next feature lands on top of it. Past 50, the risk is severe enough that most teams treat the function as a rewrite target, not a refactor target ([Sourcegraph's breakdown](https://sourcegraph.com/blog/cyclomatic-complexity-what-it-is-and-how-to-reduce-it) lays out the same four bands with the NIST citation attached).

10 is the number worth remembering. It is old (McCabe proposed it in 1976), it is conservative, and it still holds up decades later as a CI gate: below 10, defect density stays low across most of the codebases studied since. Above it, defect rates climb fast enough that "we'll fix it later" rarely happens, because the function keeps growing new branches faster than anyone circles back.

None of this means every function above 10 is broken. A `switch` statement mapping 15 enum values to 15 labels can score north of 15 while being safer than a 4-branch function tangled with side effects. The number measures path count, not danger. Read it as a prompt to look closer, not a verdict.

## Does a High Score Actually Predict Bugs?

The original 1976 McCabe paper and the studies that followed it consistently found a correlation, not a guarantee: functions above the threshold get more defects per line, on average, across large samples. Average is the operative word. Any individual function can buck the trend in either direction.

What the correlation is good for is triage, not judgment. If a bug report points at a file and you're deciding where to start reading, the function with a complexity of 34 is a better first guess than the one at 4, purely on the base rates. It's a way to spend your limited attention where the odds favor finding something, not a scorecard to wave at a teammate during review.

## Cyclomatic Complexity vs. Cognitive Complexity

A related metric, cognitive complexity, was built specifically to patch the gap the GetDX critique above points at. Cyclomatic complexity counts every branch equally: an early guard clause at the top of a function costs the same as a branch buried inside three layers of nesting. Cognitive complexity weights nesting depth, so the buried branch costs more, because it actually is harder to hold in your head while reading.

The two numbers frequently diverge on the same function. A long chain of flat `if`/`else if` statements checking the same variable can carry a high cyclomatic score and a low cognitive score, because nothing nests. A short function with two deeply nested loops can carry the opposite: a modest cyclomatic score and a cognitive score that flags it as genuinely hard to read. SonarQube computes both side by side for exactly this reason. If your linter only reports one, cyclomatic complexity is the one you'll find everywhere, but cognitive complexity is the one that better matches what a reviewer actually experiences opening the file.

## Skip: Treating One Global Threshold as Gospel

Here's the differentiator most complexity write-ups skip: a flat threshold of 10 across an entire codebase is a blunt instrument. A 40-line state machine with a dense `switch` and a 40-line function with the same score but five levels of nesting are not equally risky, even though the number is identical. Cyclomatic complexity counts paths, not depth, not variable scope, not how far apart a branch and its matching closing brace sit on the page.

[GetDX's critique](https://getdx.com/blog/cyclomatic-complexity/) makes the sharper point: teams that chase a lower score in isolation sometimes make code worse, not better, by flattening logic into deeply nested helper calls that hide the branching instead of removing it. The structural number goes down. The cognitive load required to trace what the function actually does goes up. If you're optimizing for a metric instead of for the person reading the diff in six months, you've picked the wrong target.

The fix isn't to ignore the metric, it's to pair it with a second signal: how often the file actually changes. A complex function that hasn't been touched in two years is a curiosity. A complex function edited every sprint, by three different people, is a bug incubator, and that's the distinction a raw complexity score alone can't make.

![Close-up of hands typing on a backlit mechanical keyboard in dim light](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/desktoptools/2026-08/b73c40-inline2.webp)

## The Tools That Actually Compute It

If you want the number without hand-counting branches, four tools cover most of the ground, each with a different angle on the same underlying metric. Picking between them comes down to one question: do you want the number in a CI gate, in a trend line next to your commit history, in a native app you run offline, or as a comment on the pull request itself?

SonarQube tracks complexity per function and per file over time, and its quality gates can fail a CI build the moment a pull request crosses your threshold. The Community edition is free and self-hosted, covering the core static analysis. Cloud pricing scales with lines of code, which gets expensive on a large monorepo, and the defaults are noisy until you spend an afternoon tuning them.

CodeScene takes the pairing idea from the section above and builds a product around it. Its CodeHealth score blends complexity with how often a file changes, so a hotspot report tells you which complex functions are actually dangerous, not just which ones exist on paper. The trade-off: it wants git history, not a single snapshot, so first-run value is lower than a plain linter until it has a few months of commits to learn from.

Understand, from SciTools, is a native desktop app rather than a hosted dashboard: point it at a local checkout and read cyclomatic complexity, path count, nesting depth, and Halstead metrics in one Metrics Browser. Command-line automation is gated to specific license tiers, and the UI shows its age, but for C/C++ and embedded work it goes deeper than most web-based tools bother to.

Code Climate Quality is the fastest of the four to set up: point it at a GitHub or GitLab repo and it comments directly on the pull request the moment a function crosses your threshold. The free plan covers small projects; the Team plan starts around $16.67/mo. Per-repo pricing adds up if you run a lot of small tools, and the maintainability score is a blend rather than a raw cyclomatic number, so you dig one layer to see the metric itself.

## A Terminal-Only Way to Check It

None of the above is required to get the number. `lizard` (Python, pip-installable, MIT license) walks a directory and prints complexity per function for C, C++, Java, JavaScript, Python, Go, and a dozen more languages, with zero config and zero network calls:

`# no account, no upload, no dashboard: just the numbers
$ lizard --CCN 10 src/
  NLOC    CCN   token  function
    42     12    210   parseRequestHeaders@src/http.c
    18      4     88   normalizeUrl@src/http.c
========================================
1 function exceeds CCN 10``pmccabe`, older and C/C++-only, has shipped this exact measurement since the early 1990s at Hewlett Packard. Neither tool phones home. Neither needs an account. If your workflow is "check the number before I commit," a five-second terminal command beats opening a dashboard every time, and it works the same on a plane with no wifi as it does at a desk with a fast connection.

![Home office at dusk with a monitor showing a faint dependency graph and a plant on the desk](https://fdzlnqpwsaniezitwiuw.supabase.co/storage/v1/object/public/cms-media/desktoptools/2026-08/b3dde4-inline3.webp)

## How to Bring a Score Down Without Making It Worse

Three moves account for most of the reduction you'll actually want, in order of how often they apply:

Early returns first. A function nested four `if` blocks deep almost always has the same logic available as a flat sequence of guard clauses that return early. The complexity number barely moves, sometimes it doesn't move at all, but the *reading* complexity drops hard, because you stop holding four levels of context in your head at once.

Extract the branch, not the whole function. Pulling one dense conditional block into its own well-named function takes that block's paths with it, lowering the caller's score directly. Do this for the two or three worst offenders, not for every `if` statement in the file; over-extraction just moves the branching into a maze of one-line functions that's harder to trace than the original.

Replace long `if`/`else if` chains mapping a value to an outcome with a lookup table or a `switch`. A ladder of eight `else if` statements checking the same variable against eight values is eight branches for the interpreter and eight branches for cyclomatic complexity. An object literal mapping the same eight values to the same eight outcomes is one lookup, and the complexity tool agrees.

Skip the refactor that only exists to satisfy a linter. If a function's high score comes from a genuinely irreducible decision tree, like a tax bracket calculator with eleven legitimate bands, don't scatter it across six files to dodge a threshold. Add a comment, add tests for every branch, and move on.

Order matters here more than people assume. Extracting first and adding early returns second usually leaves you with more, smaller functions that are each individually still tangled. Flatten the logic first with guard clauses, see what's left, and only then decide whether the remainder deserves its own function. Most of the time, half the original branches disappear before you extract anything at all.

## Where This Fits a Native Toolchain

We're partial to the terminal-only path for the obvious reason: no telemetry, no phone home, no dashboard tab left open in the background. Run `lizard --CCN 10 src/` in a pre-commit hook, and anything over the line fails the commit before it ever reaches a pull request. No dialog, no SaaS seat, no waiting on a CI job to tell you what a local binary could tell you in under a second.

That said, a pre-commit hook only catches new code. It says nothing about the 40,000-line file nobody has refactored since 2022. For that, a hosted tool tracking complexity trend over months earns its keep, even on a site that otherwise avoids anything that phones home.

## Should You Gate Your CI On It?

Gate on 15, not 10. A hard fail at the textbook threshold turns into noise within a month, because plenty of legitimate code (large `switch` statements mapping enum values, validation functions with many independent checks) lives comfortably above 10 without being dangerous. Set the warning at 15, the hard gate at 25, and treat anything past 50 as an automatic refactor ticket, not a discussion.

Skip the temptation to run the check once and call the job done. Complexity creeps up one small commit at a time; nobody sets out to write a 60-branch function in a single sitting. A number checked at commit time and tracked over months catches the creep before it becomes a rewrite. A number checked once during a Q1 audit catches nothing except the mess that was already there.

## FAQ

### What is a good cyclomatic complexity score?

1 to 10 is low risk and easy to test. NIST sets 10 as the recommended ceiling per function. 11 to 20 is still testable but needs real coverage, not a happy-path check. Above 20, treat the function as a refactor candidate.

### How do you calculate cyclomatic complexity?

Count the decision points in a function (if, else if, for, while, case, catch, and boolean operators like && and || inside conditionals) and add 1 for the function's single entry point. The formal graph-theory formula is M = E minus N plus 2, where E is edges and N is nodes in the control-flow graph.

### Is cyclomatic complexity the same as cognitive complexity?

No. Cyclomatic complexity counts every branch equally regardless of nesting. Cognitive complexity weights nesting depth, so a deeply nested branch scores higher than a flat one, which matches how hard a function actually is to read. The two scores frequently disagree on the same function.

### Does high cyclomatic complexity always mean bad code?

No. It correlates with defect rates on average across large codebases, but a single function with a high score, like a switch statement mapping many enum values, can be safer than a low-scoring function tangled with side effects. Use the number to decide where to look closer, not as a verdict.

### What's a free tool to measure cyclomatic complexity?

lizard is a free, MIT-licensed, pip-installable command-line tool that computes cyclomatic complexity for C, C++, Java, JavaScript, Python, Go, and a dozen other languages with zero network calls. pmccabe covers C/C++ specifically and has shipped since the early 1990s.

### How do you reduce cyclomatic complexity without making the code worse?

Start with early returns to flatten nested conditionals, which often removes branches without extracting anything. Then extract only the two or three worst offending blocks into named functions. Replace long if/else if chains checking one variable against many values with a lookup table.

### What cyclomatic complexity threshold should I set in CI?

Set a warning at 15 rather than a hard fail at 10, since plenty of legitimate code (large switch statements, validation functions with many independent checks) lives comfortably above 10. Gate hard at 25, and treat anything past 50 as an automatic refactor ticket.