# What is cyclomatic complexity?

Cyclomatic complexity counts the independent paths through a piece of code, so a higher number means more branches to understand and more cases to test.

Last updated September 29, 2026, 9 min read

## Learning objectives

After reading this article you will be able to:

-   Define cyclomatic complexity
-   Explain how it is calculated
-   Identify how it relates to testing effort

## Related content

-   [What is code review?](https://specstory.com/learning/code-review/code-review)
-   [What is static code analysis?](https://specstory.com/learning/code-review/static-analysis)
-   [What is code refactoring?](https://specstory.com/learning/code-review/refactoring)
-   [What is technical debt, and do coding agents add to it?](https://specstory.com/learning/code-review/technical-debt)
-   [What is linting?](https://specstory.com/learning/code-review/linting)

## What is cyclomatic complexity?

Cyclomatic complexity is a code metric that counts the linearly independent paths through the control flow of a function. It is also called McCabe complexity, after Thomas McCabe. The count for a function starts at 1, and each decision, e.g. an `if` statement, adds 1. A higher count means more paths for readers to follow and for tests to cover.

The International Software Testing Qualifications Board (ISTQB) [defines cyclomatic complexity](https://glossary.istqb.org/en_US/term/cyclomatic-complexity) as "the maximum number of linear, independent paths through a program." McCabe proposed it in [a 1976 paper](https://doi.org/10.1109/TSE.1976.233837) to find modules that would be hard to test or maintain.

[Static analysis](https://specstory.com/learning/code-review/static-analysis) tools report the count for each function without running the code. Teams use it in [code review](https://specstory.com/learning/code-review/code-review) to spot functions that need more tests or a simpler structure. The count describes the shape of the code, not how fast it runs or whether it works.

## How is cyclomatic complexity calculated?

Cyclomatic complexity is calculated on a function's control flow graph with the formula `E - N + 2`, where `E` counts edges and `N` counts nodes. Each node is a block of statements that run in order, and each edge is a possible jump between blocks. McCabe's general form, `E - N + 2P`, covers `P` separate graphs, e.g. a program and its subroutines.

When each decision has two outcomes, the count equals the number of decisions plus 1. Linters usually count this way, in four steps:

1.  The tool parses the source file into a syntax tree.
2.  The tool starts each function at 1.
3.  The tool adds 1 for each decision point it finds in the tree.
4.  The tool reports each function's total and flags any total above the limit.

A loop, a ternary `?:` expression, a `catch` block, and each `case` label in a `switch` also count as decisions. So does each `&&` or `||` operator, because it can skip the rest of its condition.

The count is the size of a basis set, the smallest set of paths from which every other path through the function can be built. A [structured testing report](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication500-235.pdf) by Arthur Watson and McCabe for the National Institute of Standards and Technology (NIST) makes it the minimum number of tests for basis path testing. That method is [white-box testing](https://specstory.com/learning/testing/black-box-vs-white-box-testing), because the tests come from the code's structure. Covering every branch never needs more tests than the count, and often needs fewer.

## What is an example of cyclomatic complexity?

Here is an illustrative example. Acme Co. sells furniture online. A developer asks a [coding agent](https://specstory.com/learning/ai-coding/coding-agent) to "Let customers edit their delivery address during checkout." The agent writes this function:

```js
export function changeAddress(order, address) {
  if (order.status === "paid") {
    throw new CheckoutError("Order already paid");
  }
  if (!address.postalCode) {
    throw new CheckoutError("Postal code required");
  }
  if (zoneFor(address) !== order.zone) {
    order = rebuildOrder(order.id, address);
  }
  order.address = address;
  return order;
}
```

With 3 `if` statements and no other decisions, the function scores 3 plus 1, which is 4. Its control flow graph agrees.

Diagram: Control flow graph of the checkout function

Each accented box is a decision with two exits. The 4 independent paths are the routes a basis set of tests must take.

The four paths are a paid order, a missing postal code, an address in the same shipping zone, and one in another zone. Basis path testing needs one test for each. The agent writes one [unit test](https://specstory.com/learning/testing/unit-testing), for the same zone, and it passes. A [code coverage](https://specstory.com/learning/test-quality/code-coverage) report shows that the `rebuildOrder` line never ran.

With a count of 4 and one test, a reviewer asks for the other three paths. This test covers the change of zone:

```js
test("an address in another zone keeps the cart", () => {
  const order = makeOrder({ items: 2, zone: "east" });
  const updated = changeAddress(order, westCoastAddress);
  expect(updated.items).toHaveLength(2);
});
```

The test fails with `Expected length: 2` and `Received length: 0`, because `rebuildOrder` builds the order without its items. The address saved, but the cart emptied. This example is simplified. A real checkout would also need tests for edge values on each path, e.g. a postal code with a space.

## What changes when a coding agent writes the code?

A coding agent can add decisions faster than a team reads them, which adds to [comprehension debt](https://specstory.com/learning/code-review/comprehension-debt). When a test fails or a reviewer reports a case, the agent can handle it with one more `if`, and each patch adds 1 to the count. The agent's tests can run a single path, as at Acme. A branch the agent added that no test runs is one form of [AI slop](https://specstory.com/learning/verification/ai-slop).

A complexity limit in the agent's loop has a gap of its own. When a [linting](https://specstory.com/learning/code-review/linting) rule flags a function over the limit, the agent can split it into helpers until each one passes. McCabe's paper shows that the complexity of a set of functions is the sum of their counts. The split moves the decisions without removing them, and each extra function adds 1 to the total. The paths still need tests.

A team can compare the total complexity of the functions a change touches before and after the change, not only the largest one. The team can also ask for a test on each added branch.

## What is a good cyclomatic complexity threshold?

McCabe and the NIST report both set the limit at 10 for each module. McCabe called it a reasonable limit, not a special number. Programmers on his project had to split or rewrite any module over it. The one exception he allowed was a large `case` statement with many independent cases.

The NIST report says limits as high as 15 have also worked. It reserves limits above 10 for projects with several operational advantages over typical ones, e.g. experienced staff. Its case for any limit is that "overly complex modules are more prone to error, are harder to understand, are harder to test, and are harder to modify."

Tool defaults differ:

-   **ESLint.** The `complexity` rule, which is off by default, flags a JavaScript function above 20 unless `max` sets another limit.
-   **Ruff.** Rule `C901`, which is also off by default, flags a Python function above 10 unless `max-complexity` sets another limit.
-   **gocyclo.** This Go command has no default limit. Its `-over` flag sets one and makes the command exit with code 1 when a function exceeds it.

Teams bring a function under the limit by removing decisions, e.g. with a lookup table in place of a chain of `if` statements, or by splitting it. Both are forms of [refactoring](https://specstory.com/learning/code-review/refactoring), and tests that pin the current behavior should come first.

## What are the limits of cyclomatic complexity?

The count measures only decisions, which sets its limits:

-   **It ignores nesting.** Three `if` statements nested inside each other score the same as three in a row, although the nested ones are harder to read.
-   **It overstates flat choices.** A `switch` with 12 short cases adds 12 to the count, even when each case is a single `return` statement.
-   **Tools count differently.** ESLint counts each `&&` and `||` operator, while Ruff does not count Python's `and` or `or`, so the same logic can score differently in two tools.
-   **It says nothing about correctness.** The Acme function scored 4 and still emptied the cart. A low count means fewer paths, not correct ones.

McCabe's own evidence came from one case. In a graphics system, project members ranked a set of troublesome subroutines by reliability, and that order closely matched their order by complexity. A rising count can show where [technical debt](https://specstory.com/learning/code-review/technical-debt) grows.

## How is cyclomatic complexity different from the CRAP metric?

The CRAP metric, short for Change Risk Anti-Patterns, scores a method by its cyclomatic complexity and its test coverage. Alberto Savoia and a colleague created it in 2007. Savoia gives the formula, which he calls CRAP1, in a [Google Testing Blog post](https://testing.googleblog.com/2011/02/this-code-is-crap.html):

```text
CRAP1(m) = comp(m)^2 * (1 - cov(m)/100)^3 + comp(m)
```

In the post, `comp(m)` is the method's cyclomatic complexity and `cov(m)` is its basis path coverage from automated tests, as a percentage. A score above 30 marks the method as a risk. With full coverage the score equals the complexity, and with no tests a complexity of 6 already scores 42.

Complexity alone rates the structure, while CRAP also rates whether tests run it. Coverage shows that tests ran the code, not that they checked its results. [Mutation testing](https://specstory.com/learning/test-quality/mutation-testing) checks whether the tests catch injected bugs.

## FAQs

### How do you reduce cyclomatic complexity?

Cyclomatic complexity goes down when a function makes fewer decisions, e.g. when a lookup table replaces a chain of if statements. Splitting a function also lowers each part's count, but the decisions move instead of disappearing, so the paths still need tests. Tests that pin the current behavior make either change safer.

### Does cyclomatic complexity predict bugs?

Cyclomatic complexity can point to code where bugs are more likely. McCabe and the NIST structured testing report both link high counts to errors. A count cannot show whether a given function has a bug, and the Acme checkout function emptied the cart with a low count.

### Which tools report cyclomatic complexity?

Cyclomatic complexity is reported by many linters and static analysis tools. ESLint's complexity rule covers JavaScript, a Ruff rule covers Python, and gocyclo covers Go. Tools count slightly different constructs, so compare numbers from one tool only.

### How many tests does a function with high complexity need?

A function with high complexity needs at least as many tests as its count to cover a basis set of its paths, which is the rule of structured testing. Fewer tests can often reach every branch, and more tests are needed to check edge values on each path.

---

Source: [What is cyclomatic complexity? | CRAP metric | SpecStory](https://specstory.com/learning/code-review/cyclomatic-complexity)
