Cyclomatic Complexity: What It Is and How to Reduce It
A function with 47 nested if statement branches, three switch blocks, and a loop that mutates global state. Every engineer has inherited one. You open the file, scroll past the fold, and immediately close it. That function has a cyclomatic complexity problem, and so does every team that lets overly complex code like this accumulate across hundreds of repositories without a strategy for finding it.
Cyclomatic complexity is a quantitative measure of how many independent execution paths exist through a block of code. Developed by Thomas McCabe in 1976, it remains one of the most widely used software metrics for assessing code maintainability, testability, and defect risk. But understanding the formula is only half the challenge. The real question is what you do with the number once you have it, especially when your codebase spans thousands of files across dozens of repositories.
This guide breaks down how cyclomatic complexity works, what the scores mean in practice, and how engineering teams can measure and reduce complexity at the scale of an entire organization.
What Is Cyclomatic Complexity?
Cyclomatic complexity measures the number of linearly independent paths through a program's source code. Each decision point in the control flow, such as an if, loop, case branch, or exception handler, can increase the number of independent execution paths. The more decision points, the higher the cyclomatic complexity score, and the harder the code is to test, review, and maintain.
In practical terms, a function with a cyclomatic complexity of 1 has a single straight-line path from start to finish. A function with complexity 12 has 12 distinct paths the code can take depending on input conditions. Each of those paths represents a scenario that should be tested and a potential source of bugs if missed.
The metric is grounded in graph theory. Your code's control flow can be represented as a directed graph where each node represents a basic block of sequential statements, and edges are the transitions between them. An edge connects two basic blocks when one can flow into the other through a branch, loop, or jump. Cyclomatic complexity counts the number of independent cycles in that graph.
What makes this metric useful for engineering teams is its objectivity. Unlike subjective code review comments ("this feels too complicated"), this complexity metric gives you a number. You can set thresholds, track trends over time with tools like Code Insights, and make data-driven decisions about where refactoring effort should go to improve code quality.
A Brief History of Cyclomatic Complexity
Thomas J. McCabe Sr. introduced cyclomatic complexity in his 1976 paper "A Complexity Measure," published in IEEE Transactions on Software Engineering. McCabe drew on graph theory to propose a complexity metric that could predict which modules were most likely to contain defects and be difficult to test.
His motivation was practical. NIST (the National Institute of Standards and Technology) was developing software quality guidelines for government contracts, and McCabe's metric provided a quantifiable way to evaluate code. NIST later adopted a recommended maximum cyclomatic complexity of 10 per function, a threshold many teams still use today.
By the 1990s, nearly every major IDE and linting tool calculated cyclomatic complexity. It remains one of the oldest software metrics still in active use.
How to Calculate Cyclomatic Complexity
There are two common approaches: the graph-based formula and the decision-point counting method. Both produce the same result, but the decision-point method is easier to apply by hand.
The Formula: M = E - N + 2P
The formal definition uses the control flow graph of a program:
M = E - N + 2P
Where:
- E = the number of edges (connections between code blocks)
- N = the number of nodes (code blocks)
- P = the number of connected components (typically 1, since most functions form a single connected component)
For a single function (a single program entry point), this simplifies to M = E - N + 2. You draw the control flow graph, count the nodes and edges, and plug them into the formula.
Calculating from Code (Step-by-Step Example)
Consider this Python function:
def classify_risk(score, history, account_type):
if score < 300:
return "rejected"
elif score < 600:
if history == "poor":
return "high_risk"
else:
return "medium_risk"
else:
if account_type == "premium":
return "low_risk"
elif history == "excellent":
return "low_risk"
else:
return "standard"
Now, let's break it down into a graph to help walk through the calculation:

Step 1: Identify each decision point. This function has five decision points: score < 300, score < 600, history "poor", account_type "premium", and history == "excellent".
Step 2: Apply the shortcut formula. Cyclomatic complexity = number of decision points + 1 (since this is a single connected component), so in this case it would be: 5 + 1 = 6.
Step 3: Interpret the result. A score of 6 falls within the "simple, low risk" range. This function is testable and maintainable without refactoring.
Quick Method: Counting Decision Points
For most day-to-day use, you do not need to draw a control flow graph. Count the number of decision keywords in the function and add 1.
Decision keywords to count:
if, elif, else iffor,while,docase(inswitchstatements)catch,except&&,||(each logical operator in a compound condition)- Ternary operators (
?:)
This is a useful quick estimate that takes seconds to apply, though exact counting rules depend on the language and the analyzer. Imperative languages like Java and Python follow these rules closely, but some tools count compound boolean operators separately; others do not. For simple structured programs, the result matches the formal graph formula.
What Is a Good Cyclomatic Complexity Score?
The answer depends on context, but McCabe's original thresholds remain a widely used starting point.

Common Thresholds
NIST recommends a maximum cyclomatic complexity value of 10 per function. Many teams use this as a hard gate in CI/CD pipelines, rejecting pull requests that introduce functions with high complexity scores above the threshold.
When High Complexity Is Acceptable
Not every high score indicates a problem. State machines, protocol parsers, and configuration validators often have legitimately high cyclomatic complexity because their logic requires many branches.
A function that maps 30 error codes to recovery actions will have a cyclomatic complexity of at least 30. Refactoring it into 30 separate functions does not reduce actual complexity. It just scatters it. The same applies to protocol handlers that must respond to many distinct message types, input validation functions that check dozens of fields, or tax calculators that branch on jurisdiction-specific rules.
The test is whether the complexity is essential or accidental. Essential complexity exists because the problem domain is inherently branchy. Accidental complexity exists because the code was poorly structured. Focus refactoring effort on accidental complexity, where the payoff is real.
When you decide to keep a high-complexity function, compensate with thorough test coverage. If a state machine has a complexity of 30, it needs 30+ test cases covering each branch. Document the decision to accept the complexity (a code comment or ADR explaining why refactoring would not improve the code) so future developers do not waste time re-evaluating the same trade-off.
Why Cyclomatic Complexity Matters
High cyclomatic complexity correlates with real engineering costs: more bugs, slower onboarding, longer code reviews, and higher technical debt. Understanding these connections helps teams prioritize where to invest refactoring effort.
Impact on Testing and Bug Density
A function with a cyclomatic complexity of 25 has at least 25 linearly independent paths, which gives a sense of the minimum number of test cases needed for basis path testing. In practice, writing enough unit tests to cover all those logical paths is rare. Most teams write far fewer tests per function, which means paths go untested and bugs hide in the gaps. The testing effort scales linearly with complexity, but engineering budgets do not. NIST's recommendation of 10 reflects the point where thorough testing starts to become impractical for most teams.
Research supports the connection between complexity and defects, though with nuance. Basili and Perricone's 1984 study at NASA found that modules with higher cyclomatic complexity had a significantly higher density of errors during development. However, later research in software engineering showed that lines of code is a comparable predictor of bugs. The value of cyclomatic complexity is not that it perfectly predicts defects. It is that it identifies the specific functions where defects are most likely to concentrate, giving teams a targeted starting point rather than reviewing code at random.
Maintainability and Developer Onboarding
When a new software developer joins your team and opens a function with a cyclomatic complexity of 40, they face 40 possible paths to understand before they can safely make changes. High-complexity functions are a common source of "don't touch that code" warnings that slow down onboarding and create knowledge silos.
A single high-complexity function might be manageable. But when you have hundreds of them scattered across dozens of repositories, the aggregate effect on developer productivity is significant. Engineering leaders often underestimate this cost because software complexity is distributed across repositories rather than concentrated in one place. Most teams only discover these hotspots when someone happens to open the file. Tools like Code Search let you search for complexity patterns (deeply nested conditionals, functions with excessive branching) across every repository at once, turning a reactive discovery process into a proactive one. Lyft used this approach to refactor and verify code across 2,000+ repositories during their monolith-to-microservices migration.
Code Review and Pull Request Quality
Pull requests that introduce high-complexity functions take longer to review and are more likely to receive "LGTM" approvals without thorough examination. Reviewers face cognitive overload when trying to reason about 20+ execution paths in a single function. The natural response is to skim rather than scrutinize.
Setting complexity thresholds in your CI pipeline converts this subjective concern into an automated check. When a PR adds a function with a cyclomatic complexity above your team's threshold, it gets flagged before a reviewer even opens it.
How to Reduce Cyclomatic Complexity
Reducing complexity is a refactoring exercise. The goal is to refactor complex code by breaking apart large functions with many branches into smaller, focused units that are easier to test and understand independently. Here are the most effective patterns.
Extract Methods and Functions
The most straightforward technique. Identify a block of code inside a branch that performs a distinct task, and move it into its own function. For example, this code initially has a complexity score of 8:
def process_order(order):
if order.status == "new":
if order.payment_verified:
if order.inventory_available:
ship(order)
else:
backorder(order)
else:
reject_payment(order)
elif order.status == "returned":
process_return(order)
But, after a refactor, we can reduce the complexity to 6: complexity 3 (main) + 3 (handle_new_order).
def process_order(order):
if order.status == "new":
handle_new_order(order)
elif order.status == "returned":
process_return(order)
def handle_new_order(order):
if not order.payment_verified:
reject_payment(order)
elif not order.inventory_available:
backorder(order)
else:
ship(order)
The total number of paths has not changed, but each function is now independently testable and easier to reason about during code review. The trade-off is that over-extracting can scatter logic across many small functions, increasing overall complexity of the call graph even as individual functions simplify. Extract when the inner block has a clear, nameable responsibility. If you struggle to name the extracted function, the extraction may not be worth it.
Replace Conditionals with Polymorphism
When a function branches on the type of an object, replace the conditional with polymorphism. Each type gets its own class with its own method, and the branching logic disappears from the calling code.
This pattern is most effective when the same type-check appears in multiple functions. Centralizing the behavior in the type itself reduces complexity across every call site simultaneously. For teams managing large codebases, Batch Changes can apply this pattern across all affected repositories at once rather than refactoring one file at a time.
Use Guard Clauses and Early Returns
Guard clauses invert the nesting pattern. Instead of wrapping the main logic in nested structures, check for invalid cases first and return early. For example, before refactoring, this logic is deeply nested:
def calculate_discount(user, order):
if user.is_active:
if order.total > 100:
if user.membership == "gold":
return order.total * 0.2
else:
return order.total * 0.1
else:
return 0
else:
return 0
After we refactor it to be flatter with guard clauses, the complexity is reduced:
def calculate_discount(user, order):
if not user.is_active:
return 0
if order.total <= 100:
return 0
if user.membership == "gold":
return order.total * 0.2
return order.total * 0.1
The cyclomatic complexity is the same in both versions, but code readability improves significantly because the nesting depth drops from 3 to 1. This is a case where cognitive complexity (how hard the code is to understand) improves even when the cyclomatic complexity metric stays constant.
Simplify Nested Logic
Look for opportunities to combine conditions, use lookup tables instead of long switch statements, and replace flag variables with direct returns.
Lookup tables are particularly effective for replacing large switch/case blocks. For example, when using a switch statement in the code below, the complexity is rated as a 6:
def get_status_label(code):
if code == 1:
return "Active"
elif code == 2:
return "Pending"
elif code == 3:
return "Suspended"
elif code == 4:
return "Cancelled"
elif code == 5:
return "Archived"
return "Unknown"
After we refactor this to a lookup, the complexity drops to 1:
STATUS_LABELS = {1: "Active", 2: "Pending", 3: "Suspended",
4: "Cancelled", 5: "Archived"}
def get_status_label(code):
return STATUS_LABELS.get(code, "Unknown")
This technique works best for functions that map inputs to outputs without side effects. Be cautious with lookup tables that hide business logic since a switch statement with comments explaining each case may be clearer than a dictionary with cryptic values. The right choice depends on whether the mapping is simple data or encodes domain rules that benefit from inline documentation.
Cyclomatic Complexity vs. Cognitive Complexity
Cyclomatic complexity counts paths. Cognitive complexity measures how hard the code is for a human to understand. These are related, but different things, and modern teams should track both.
Cyclomatic complexity treats all branches equally. A flat series of if/else if blocks and deeply nested if statements inside for loops produces similar scores, even though the nested version is dramatically harder to understand. Cognitive complexity addresses this gap. Developed by SonarSource, it adds a penalty for nesting depth and weights structures that break linear reading flow.
Consider two functions that both have a cyclomatic complexity of 5:
Function A has five sequential if blocks that each return early. It reads top-to-bottom and is easy to follow. Its cognitive complexity might be 5.
Function B has a for loop containing a try/catch with an if/else inside the catch block, plus an if at the outer level. It has the same five decision points but requires you to hold multiple contexts in your head simultaneously. Its cognitive complexity might be 12.
The practical takeaway: use cyclomatic complexity for automated thresholds and software testing requirements, and use cognitive complexity for code review and readability assessments. Together, they give a more complete picture than either traditional metrics like lines of code or either complexity measure alone.
Tools for Measuring Cyclomatic Complexity
Measuring cyclomatic complexity for a single file is straightforward. The challenge is cyclomatic complexity analysis at scale: measuring it consistently across your entire codebase and tracking it over time.
IDE and Editor Integrations
Most development environments calculate cyclomatic complexity per function:
- Visual Studio includes built-in code metrics that display cyclomatic complexity for .NET projects
- IntelliJ IDEA and other JetBrains IDEs support complexity metrics through plugins like MetricsReloaded
- ESLint with the complexity rule flags JavaScript and TypeScript functions that exceed a configurable threshold
- Pylint and Radon calculate cyclomatic complexity for Python codebases
- SonarLint provides in-editor complexity feedback connected to SonarQube rules
These tools work well for individual files and active software development. They catch complexity problems before code is committed, keeping the testing process manageable.
CI/CD Pipeline Integration
To enforce complexity thresholds across a team, integrate measurement into your build pipeline:
- SonarQube analyzes cyclomatic complexity across 30+ languages and can gate pull requests that exceed thresholds
- CodeClimate includes complexity as part of its maintainability scoring
- Custom linter rules in ESLint, Pylint, or language-specific tools can fail builds when complexity exceeds your team's agreed limits
Pipeline integration converts complexity from a suggestion into a requirement, embedding code coverage for complexity into the software development lifecycle. The trade-off is that hard gates can slow down development if thresholds are set too aggressively. Start with a warning at 15 and a hard gate at 25. Tighten as your team's codebase improves.
Cross-Repository Complexity Analysis
IDE tools measure complexity in the file you have open. CI/CD tools measure complexity in the repository being built. But neither answers the question: where are the highest-complexity functions across your entire organization?
Answering that question requires a cross-repository search. Sourcegraph is one approach: instead of waiting for individual developers to discover complex code one file at a time, teams search for complexity patterns across every repository simultaneously.

Finding complex code at scale. Code Search supports regex and structural patterns that identify complexity signals across thousands of repositories. You can search for deeply nested conditionals, functions with excessive branching, or specific anti-patterns that indicate high cyclomatic complexity. Deep Search helps investigate natural-language questions about complex code patterns across your codebase, while Code Search and structural search remain the more precise tools for pattern-based discovery.
Tracking complexity trends. Code Insights lets you create dashboards that track search-based complexity signals over time. Monitor whether the number of matches for high-complexity patterns is increasing or decreasing across sprints, and measure the impact of refactoring initiatives with data instead of guesswork.
Refactoring at scale. Once you have identified complexity hotspots, Batch Changes can automate and track large-scale refactoring changes across all affected repositories. Instead of filing tickets for each team to fix their own high-complexity functions, you open pull requests across every affected repository and track them to completion.
Understanding complex code paths. Code Navigation provides cross-repository go-to-definition and find-references powered by SCIP. When you encounter a high-complexity function, you can trace its callers and dependencies across repository boundaries to understand the full impact before refactoring.
Conclusion
Cyclomatic complexity is not a perfect metric. It does not capture nesting depth, readability, or the cognitive effort required to understand a function. But it is objective, automatable, and widely supported by tooling. When combined with cognitive complexity and tracked consistently across the software development lifecycle, it becomes a practical signal for directing refactoring effort to build more maintainable software.
The biggest limitation has never been the metric itself. It is that most teams only measure it in one file at a time. A function with a complexity of 35 gets flagged when someone happens to open it. Hundreds of similar functions sit undiscovered in other repositories.
Closing that visibility gap is where the real leverage is. Get started with Sourcegraph to search for high-complexity patterns across all your repositories, track trends over time with Code Insights, and apply refactoring patterns at scale with Batch Changes.
Frequently Asked Questions
What do you mean by cyclomatic complexity?
Cyclomatic complexity is a software metric that counts the number of linearly independent paths through a function's source code. Developed by Thomas McCabe in 1976, it uses the formula M = E - N + 2P (edges minus nodes plus two times connected components) on the code's control flow graph. A higher score means more branches, more required test cases, and typically harder maintenance.
What is a good cyclomatic complexity?
A cyclomatic complexity between 1 and 10 is generally considered good. NIST recommends a maximum of 10 per function. Scores between 11 and 20 warrant review, and anything above 20 is a strong candidate for refactoring.
Does cyclomatic complexity measure code quality?
It measures one dimension: structural complexity. As a code complexity metric, cyclomatic complexity correlates with testability and defect density, but it does not capture readability, architecture, documentation, or abstraction quality. Use it as one signal among several, not as a definitive quality score.
How do you calculate cyclomatic complexity?
Count every decision point in a function (if, for, while, case, catch, &&, ||) and add 1. A function with three if statements and one for loop has a cyclomatic complexity of 5. The formal graph method uses M = E - N + 2P but produces the same result.
.avif)