Skip to main content
CodePulse
All Guides
Code Quality

Cyclomatic Complexity Explained (With Real Thresholds)

What cyclomatic complexity actually measures, how it is calculated, the widely cited risk bands, and where it falls short as a readability signal.

Ashley RussellAugust 25, 202610 min read
Cyclomatic Complexity Explained (With Real Thresholds) - visual overview

Find quality and ownership risk before it becomes an incident

Preview how CodePulse combines review depth, risky changes, hotspots, and knowledge concentration without turning activity into individual scores.

Preview the Sample Report·Analyze My GitHub Data

Read-only GitHub · Your code stays yours · Team-level by default · No credit card

Cyclomatic complexity is the oldest and still most widely reported code complexity metric: a count of how many independent paths run through a function's logic. It's cheap to compute, easy to explain in a code review comment, and - on its own - a surprisingly weak predictor of whether a given function is actually hard to work with. Understanding what the number does and doesn't tell you matters more than memorizing the formula.

What is cyclomatic complexity?

Cyclomatic complexity, introduced by Thomas McCabe in 1976, counts the number of independent paths through a function's control flow: every if, loop, case, and boolean condition adds one to a base score of 1. Scores of 1-10 are considered simple, 11-20 moderate, 21-50 complex, and above 50 effectively untestable, since the number of test cases needed to cover every path grows past what teams realistically write. It's most useful as a testing-coverage signal - how many test cases a function needs - and less useful as a readability signal, which is what the newer cognitive complexity metric was built to address.

The metric predates most of the languages it's now measured in. McCabe was working with FORTRAN control-flow graphs in 1976, and the underlying idea - a function's branch count maps to how many tests you need to exercise every path - has survived essentially unchanged into every modern linter's complexity rule.

Cyclomatic complexity answers "how many tests does this need?" It was never built to answer "how hard is this to read?" - and treating it as if it does is where most teams go wrong with the number.

How Is Cyclomatic Complexity Calculated?

The formal definition comes from graph theory: M = E - N + 2P, where E is the number of edges in the control-flow graph, N the number of nodes, and P the number of connected components (almost always 1 for a single function). In practice, no tool builds an explicit graph anymore - they walk the abstract syntax tree and increment a counter for every decision point: if, else if, for, while, case, ternary expressions, and boolean operators (&&, ||) inside a condition. A function with zero branches starts at 1; each additional decision point adds one.

1startif (a)+1for (i)+1case x, y+26Base of 1, plus one for each decision point along the way1-10simple11-20moderate21-50complex50+untestable
Each decision point adds to the count; the widely used bands run from "simple" to "effectively untestable" as the score climbs.
Detect code hotspots and knowledge silos. See an example report

Why Does It Matter, and Where Does It Fall Short?

Two findings, replicated across decades of empirical software-engineering research, keep cyclomatic complexity relevant despite its age. First, it correlates with defect density: more independent paths generally means more paths nobody wrote a test for, and untested paths are where bugs live. Second, it correlates with modification time: a reviewer or a future maintainer takes measurably longer to safely change a function with twenty branches than one with three.

The correlation is real but loose, and that looseness is the metric's main weakness. A long, flat function with ten sequential if statements that each handle one independent case can score as "complex" while reading easily top to bottom, because each branch is simple and there's no nesting to track. A short function with three deeply nested conditionals can score lower while being genuinely hard to follow, because nesting depth - not raw branch count - is what actually taxes a human reader's working memory. Cyclomatic complexity was designed to answer "how many tests does this need," and it does that reasonably well. It was never designed to answer "how hard is this to read," which is the gap cognitive complexity was built to close.

Score rangeRisk levelTypical action
1-10Simple, low riskNo action needed
11-20ModerateWorth a second look in review
21-50Complex, high riskFlag for refactoring before the next change
50+Effectively untestableSplit the function before adding anything else to it

* Our Take

A CI gate on cyclomatic complexity is worth having. A team that treats the number as a quality score is measuring the wrong thing.

Use it as a testing-coverage tripwire - a function above 20 needs more test cases than most teams actually write, so flag it and either add tests or split it. Don't use it as a proxy for "this file is a mess" on its own; pair it with cognitive complexity for readability and with real change history (which files people actually struggle with in practice) before deciding what to refactor first.

See where your delivery slows down, and why. See an example report

Where Does CodePulse Fit?

CodePulse doesn't parse function bodies or compute a cyclomatic complexity score - that's squarely a static-analysis job, and tools built for it (SonarQube, CodeClimate, language-specific linters) do it well. What CodePulse tracks from the same GitHub history is the signal that tells you where to point that tooling first: which files change constantly, how concentrated ownership is, and how much churn a file absorbs release after release. High-complexity code and high-churn, single-owner code overlap often enough that the two views are worth running side by side.

* How to See This in CodePulse

Navigate to File Hotspots to see the files most worth running a complexity scan against:

  • Change frequency and total lines changed per file, ranked by risk level
  • Unique contributor count, so you can see whether a hot file also has thin ownership
  • Pair it with code churn trends to see whether the same files keep resurfacing release after release

A complexity score tells you a function is hard to test. Change history tells you whether anyone's actually paying that cost yet.

For the readability half of this picture, see our cognitive complexity guide. For the broader discipline both metrics feed into, see what quality engineering actually covers or our bus factor and knowledge silo guide for the ownership-concentration angle on the same risky files.

If you want to see your own file hotspots before deciding what to refactor, start a free CodePulse trial. It reads directly from your GitHub history, so there's nothing to configure before you see real numbers.

Frequently Asked Questions

The widely cited bands, from McCabe's original 1976 paper and later tooling defaults, are: 1-10 is simple and low risk, 11-20 is moderate and worth a second look, 21-50 is complex and high risk, and above 50 is considered untestable in practice - the number of test cases needed to cover every path grows past what any team actually writes. Most linters (ESLint's complexity rule, SonarQube, CodeClimate) default their warning threshold somewhere in the 10-15 range for a single function.

CodePulse dashboard highlighting the team's biggest delivery bottleneck

See which files and owners carry your real risk

Connect GitHub and get your churn hotspots, single-owner modules, and review-depth signals ranked by exposure. No repository cloning, no file contents read.

Read-only GitHub · Your code stays yours · Team-level by default · No credit card