Cyclomatic Complexity: What to Build or Refactor Next

Summary

Cyclomatic complexity is a 1976 metric that counts independent execution paths through a function. Scores above 10 need tests; above 20, refactor before adding anything new; above 50, rewrite. Used as a decision map rather than a quality grade, it gives you a priority queue: the high-CC functions your next feature touches are your actual sprint plan. This article shows how to run the scan, read the results, and turn complexity data into a shipping decision.

You open the codebase at 22:00. You know what feature you want to ship next. Twenty minutes in, you're debugging something unrelated. The function you're editing is 300 lines long, handling six different cases, and nobody -- including you -- remembers why it grew this way.

Cyclomatic complexity (CC) is the metric that would have flagged this function months ago. It counts independent execution paths through your code. Functions above CC 10 need tests before you touch them. Above CC 20, you refactor before adding anything. Above CC 50, you rewrite. That's the decision tree. The rest of this article explains how to run it on your project today and turn the output into a sprint plan.

What cyclomatic complexity actually measures

Thomas McCabe published the metric in 1976. The formula is M = E - N + 2P, where E is edges, N is nodes, and P is connected components in the control flow graph of a function. In practice: start at 1, add 1 for each if, else if, for, while, case, catch, &&, or ||. That's the score.

A function with no branching has CC = 1. A function that checks ten conditions, iterates over a list, and handles three error types will land around CC 15-20. A function built by six devs patching each other's logic over two years can hit CC 60.

The metric doesn't capture everything. It doesn't measure nesting depth, naming quality, or abstraction clarity. A function can have low CC and still be hard to read. But a function with CC above 25 is almost always hard to read. The correlation in one direction is consistent enough to be useful as a priority filter.

The thresholds that decide your next move

Most style guides and static analysis tools converge on similar ranges:

NIST's original recommendation was a ceiling of 10 per function. In practice, most engineers treat 15 as a realistic soft limit for production code, and anything above 20 as a blocker for new work touching that function.

The table is your decision tree for what to build next. When your backlog is full and you can't decide whether to add a feature or fix an existing module, run a complexity scan first. The functions sitting above 20 are your mandatory stop before anything new touches them.

How to run your first scan in 5 minutes

Every major language ecosystem has a CLI tool ready. You don't need a SaaS dashboard for this first step:

Python (Radon):

pip install radon
radon cc -s -a ./src

JavaScript / TypeScript (ESLint):

// .eslintrc
{ "rules": { "complexity": ["error", 10] } }

Then run eslint ./src --ext .ts,.js in your pipeline.

Go (gocyclo):

go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
gocyclo -over 10 ./...

Java / Kotlin (PMD):

pmd check -d ./src -R rulesets/java/quickstart.xml | grep CyclomaticComplexity

The output lists every function sorted by CC score. What you're looking for: the top 10 highest-scoring functions in your project. Save that list. That's your priority queue for the next sprint.

Abstract visualization of code branching paths with complexity heatmap

Reading the report as a decision map

High-CC functions cluster in predictable places. In a recommendation API I worked on -- roughly 8,000 lines -- running radon cc -s surfaced three functions with CC above 30. All three were in the data normalization layer. That layer had been patched incrementally over six months, each time for a slightly different data source edge case. Nobody had ever looked at the cumulative damage.

Every new feature that touched the normalization layer took 40% longer to ship. We spent two days before the next sprint refactoring those three functions, bringing CC scores from 32, 28, and 31 down to 7, 5, and 8. The following sprint was the fastest in four months.

Three functions. Two days. Four months of drag, explained.

The pattern is consistent across codebases: roughly 10-15% of functions hold 70-80% of the complexity in any organically grown project. That concentration is your real roadmap. Before committing to a new feature, run the scan. If your feature touches a function above CC 15, you have a concrete choice: ship it slowly into risky code now, or spend two days refactoring and ship it properly over the following week. The math usually favors the refactor.

For side projects specifically, this matters more than it does in team settings. You can't hand a gnarly function to a colleague. The full cognitive cost of re-understanding CC 30 code falls on you, usually at 22:00, six months after you wrote it.

The second thing complexity reports do well: they give you a concrete answer when you're paralyzed between "add a feature" and "clean up first." The scan removes the ambiguity. If the feature touches high-CC code, you clean first. If it doesn't, you ship. That's a decision, not a debate.

When high complexity is the right call

Not every high-CC function is a problem.

A payment state machine handling 12 transaction states genuinely needs to handle 12 states. A parser processing 15 grammar rules has 15 real cases. Flattening those into 15 helper functions with CC 1 each doesn't reduce complexity -- it distributes it across files that are now harder to navigate together.

The useful question is not "is this CC too high?" but "is this CC higher than the domain requires?" A routing function handling 15 URL patterns with CC = 18 is probably appropriate. A user profile update function with CC = 18 that grew by accumulated patches to handle edge cases nobody planned for is a different problem.

Keep CC under 15 on your critical paths. Let it run higher in genuinely complex domains, and document the reason inline. The functions that surprise you later are always the ones with no justification for their complexity other than accumulated fixes.

Developer analyzing code quality metrics on dual monitors in a dark workspace

Add complexity checks to CI and stop debugging blind

Running the scan manually once is useful. Hooking it into CI is what actually changes behavior over time.

The practical setup: set a threshold in your linter config, fail the build if any new function exceeds it, and save the complexity report as a CI artifact on every run. The list exists every build, without anyone remembering to generate it.

For GitHub Actions with a Python project:

- name: Complexity check
  run: |
    pip install radon
    radon cc -n C -s ./src
    if [ $? -ne 0 ]; then exit 1; fi

For JavaScript with ESLint already in the pipeline, add "complexity": ["error", 12] to your rules. Any PR pushing a function above 12 fails automatically.

A practical team standard: CC <= 15 as a hard CI block, CC 11-15 as a warning requiring an inline comment explaining the business reason. That second tier forces the conversation without making every code review a negotiation about threshold exceptions.

Three tools worth knowing for ongoing tracking

The CLI tools above give you a one-time snapshot. For ongoing project-level visibility, three tools stand out:

SonarQube is the most complete option for project-wide complexity tracking. The community edition covers cyclomatic complexity, cognitive complexity, and test coverage in one dashboard. Quality gates can block a merge if new code pushes complexity above a threshold. It's the tool to reach for first when a project has more than two contributors and a real review process.

CodeScene correlates complexity scores with commit history, surfacing functions that are both complex and changed frequently. Those intersections are your real technical debt -- not just hard to understand, but actively costing time on every sprint. If you need to make an argument for a refactoring sprint on a team's roadmap, CodeScene's output is the argument.

Code Climate Quality is the lighter option, suited for solo projects and open-source repos. GitHub integration works out of the box, defaults are sensible, and the free tier for public repos is usable. If you're managing a side project and want complexity drift tracked without standing up infrastructure, start here.

The decision cyclomatic complexity actually forces

Cyclomatic complexity doesn't tell you whether your code is good. It tells you where the risk is concentrated. That's a more useful piece of information.

Run the scan on your current project before your next sprint. Look at the top 10 functions by score. Ask which ones your next feature touches. If the answer is "three of them, all above CC 20," your sprint plan is clear: bring those three functions under CC 10, then ship the feature. It will take half the time it would have otherwise.

The corollary for builders: if you're looking for your next side project and you work inside real codebases, your complexity report is a spec. The most complex module blocking velocity -- one that's complex and touched constantly -- is a tool worth building around. A lightweight complexity dashboard that correlates CC scores with git blame data and changed-file frequency, priced for a team of two rather than an enterprise contract, is a project that doesn't fully exist yet. The tooling gap is real.

That's not a pitch. It's a pattern: the tools you want but can't find are often the side projects most worth shipping.

Frequently asked questions

What is a good cyclomatic complexity score?
A CC between 1 and 10 is generally considered acceptable. NIST's original recommendation was a ceiling of 10 per function. Scores between 11 and 20 warrant review and test coverage before modification. Anything above 20 is a strong candidate for refactoring before new feature work touches the same code.
How do you calculate cyclomatic complexity?
The formula is M = E - N + 2P, where E is edges, N is nodes, and P is connected components in the control flow graph. In practice, start at 1 and add 1 for each if, else if, for, while, case, catch, and logical operators && and ||. Every major static analysis tool does this automatically.
How does cyclomatic complexity affect side projects?
In solo builds, high CC compounds fast. You have no team to share context with. A function written at CC 30 four months ago will cost a day of re-understanding before you can safely modify it. Keeping critical path functions under CC 15 is one of the highest-leverage investments in future shipping velocity.
Is cyclomatic complexity the same as cognitive complexity?
No. Cyclomatic complexity counts independent paths through control flow, which maps directly to the minimum number of test cases needed. Cognitive complexity, developed by SonarSource, weights nesting depth and control flow breaks -- it better reflects the mental effort of reading code. Both are useful; CC is more universally supported in tooling.
When should you rewrite instead of refactor?
When a function has CC above 50 and no meaningful test coverage, refactoring safely requires tests you cannot write because the code is too entangled to test in isolation. A rewrite with tests from scratch is often faster and safer. Most engineers put the practical inflection point between CC 40 and 60.
What tools measure cyclomatic complexity?
Radon for Python, ESLint complexity rule for JavaScript and TypeScript, gocyclo for Go, PMD for Java, SonarQube for multi-language enterprise projects, Code Climate Quality for a lighter multi-language option, and CodeScene for complexity correlated with git history. Most CI platforms integrate with at least one of these.
Can cyclomatic complexity be artificially too low?
Yes. Extracting every branch into a one-line helper drops the CC of the original function without reducing real complexity -- it scatters it across files. A function with CC 1 that delegates all branching to opaque helpers is misleading. Use CC as one signal alongside readability review, not as a number to minimize at any cost.