Prometheus is a 5,556-function Go codebase where my analysis flagged 1,020 functions as critical band, and the top of that list is dominated by one pattern: functions with 70-plus execution paths. The number one hotspot, append in scrape/scrape.go, carries an activity-weighted risk score of 23.12 with a cyclomatic complexity of 138 and was last touched just 11 days ago — that combination is what separates a live regression risk from a function that merely looks intimidating. Four of my top five hotspots sit in the ‘fire’ quadrant, meaning they’re actively being changed while carrying god-function-level complexity, so I’d treat this week’s PRs against scrape.go, engine.go, db.go, and promtool’s config checker with extra review weight before merging. The fifth, FromMetrics in the OTLP write path, is a different kind of risk: it’s in the ‘debt’ quadrant, just as complex but untouched for 33 days — the danger there is blast radius the next time someone has to change it, not this week’s commits.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
append | scrape/scrape.go | 23.1 | 138 | 6 | 54 |
eval | promql/engine.go | 19.5 | 237 | 6 | 96 |
FromMetrics | storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go | 19.3 | 82 | 7 | 53 |
loadWAL | tsdb/agent/db.go | 19.1 | 73 | 6 | 38 |
checkConfig | cmd/promtool/main.go | 19.0 | 81 | 7 | 14 |
Large Repo Analysis
prometheus is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.
5,556 functions analyzed
The quadrant split is the first thing worth sitting with: 781 functions across the codebase are both complex and actively changing, and 1,400 more are complex but currently dormant — structural debt waiting for the next person who has to touch them. My top five hotspots are drawn from the 1,020-function critical band, and four of the five are in the fire quadrant, which is the segment I’d prioritize this sprint over anything in debt.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×10Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Exit Heavy×10Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×10God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×10Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Cyclic Hub×1Cyclic Hub
Participates in a call cycle with other high-traffic functions, creating circular dependency risk.Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.Stale Complex×1Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
Every one of my top five functions is tagged complex branching, deeply nested, exit heavy, god function, and long function simultaneously. That’s not five isolated findings — it’s one recurring shape: large functions with many independent execution paths, deep conditional nesting, and multiple early returns. In Go specifically, exit-heavy functions with this many branches usually mean a correspondingly large number of explicit error-return statements, since Go’s if err != nil idiom turns every fallible call into its own branch. That has a direct testing implication: each of those paths is a required test case if you want real coverage, not just line coverage.
append — scrape/scrape.go
This is the single highest-risk function in the repository, and it’s also the only one in my top five carrying both the ‘cyclic hub’ and ‘hub function’ patterns, meaning it sits at a coupling center other code depends on heavily. The source shows why: it’s a state machine over a streaming parser, decoding one metric family entry at a time inside a for loop with a switch on entry type, coordinating sample limits, exemplars, histogram variants, and staleness markers all in one body. A cyclomatic complexity of 138 with a fan-out of 54 means this function calls 54 distinct other functions and has well over a hundred distinct paths through it — an enormous surface for a single review to hold in working memory.
The historical signals add useful context without indicting anyone: the file has a bug-fix rate of 17.11% across 76 total commits, with 13 distinct authors touching it in the last 90 days and a review-comment density of 18 — high enough to suggest reviewers already find this code hard to reason about in PRs. It was touched once in the last 30 days and last changed 11 days ago, so it’s live, not stale. My recommendation: extract the per-entry-type handling (series, samples, histograms, float histograms) out of the main loop into named helper functions. That won’t reduce the real work being done, but it will let each path be tested and reviewed independently instead of as one 138-path monolith.
eval — promql/engine.go
This is the highest raw complexity in the whole data set — cyclomatic complexity of 237, fan-out of 96 — even though its activity-weighted risk score of 19.48 lands slightly below append’s. The source excerpt confirms the shape: a large type-switch over PromQL AST node kinds (AggregateExpr, Call, and presumably others below the excerpt), each branch running its own nested logic for grouping, matrix-argument detection, and recursive evaluation calls back into eval itself. That kind of recursive god-function is exactly where a fan-out of 96 becomes a real coupling problem: touching any one AST-node branch risks unintended interaction with how another branch’s recursive call is set up.
Three touches in the last 30 days and only 10 days since the last change put this solidly in the fire quadrant. The file’s bug-fix rate of 29.17% across 144 commits is the highest of any function in my top five, and one recorded revert stands out — I wouldn’t read that as proof this exact function is defective, but it does mean the file it lives in has a real history of changes needing correction. Given 17 distinct authors in the last 90 days, I’d push for splitting each AST-node case into its own well-named evaluation function before the next round of PromQL feature work lands here.
FromMetrics — storage/remote/otlptranslator/prometheusremotewrite/metrics_to_prw.go
This is the one debt-quadrant entry in my top five: it hasn’t been touched in 33 days, with zero touches in the last 30, so this is not a live regression risk this week — it’s structural debt that will have high blast radius whenever someone next needs to modify OTLP-to-Prometheus metric conversion. The nesting depth of 7 is the deepest in my top five, and the source shows why: nested loops over resource metrics, then scope metrics, then individual metrics, with per-iteration error handling via errors.Join at each level. That’s a lot of accumulated state (earliest/latest timestamp tracking, temporality validation, name-collision bookkeeping) carried across four nesting levels.
With only 3 distinct authors in the last 90 days and one recorded revert against 35 total commits, this reads as narrowly-owned, complex conversion logic that doesn’t get much cross-team scrutiny. My recommendation: before the next OTLP schema change forces someone into this function, extract the per-metric-type handling (the nolint comment around temporality validation is itself a signal that the logic there was already judged awkward) into a separate reviewable unit.
loadWAL — tsdb/agent/db.go
This one carries a concurrency dimension the structural numbers alone don’t capture. The source shows a producer goroutine decoding WAL records (series, samples, histogram variants) into a channel, feeding a consumer loop that ranges over that channel — classic Go fan-out/fan-in via channels, with a separate error channel for corruption signaling. Getting the decode-then-signal ordering right across that goroutine boundary is exactly the kind of risk that cyclomatic complexity and fan-out (73 and 38 respectively) under-represent, since the metrics count branches and calls, not the timing between two concurrently running code paths.
It was last changed only 3 days ago with one touch in the last 30 days, and the file’s bug-fix rate sits at 17.5% across 40 commits with 5 distinct authors recently active. Given the goroutine handoff, I’d want any review here to specifically trace what happens to the decode goroutine if the consumer loop returns early on a corruption error — that’s the kind of interaction unit tests on the function in isolation tend to miss.
checkConfig — cmd/promtool/main.go
The shape here is different from the other four: fan-out of only 14, the lowest in my top five, but nesting depth of 7 ties for the deepest. The source confirms a config-validation flow with rule-file globbing, scrape-config resolution, then a nested loop over scrape configs, over their service-discovery configs, over a type-switch on SD config kind (Kubernetes, file-based, and presumably more), with file-glob handling nested another level inside the file-SD case. That’s depth from nested validation logic rather than breadth from calling many collaborators, which changes the fix: this isn’t a coupling problem, it’s a decompose-conditional problem.
Two touches in the last 30 days and 11 days since last change puts it in the fire quadrant alongside append, eval, and loadWAL. With a bug-fix rate of 18.75% across 48 commits and one recorded revert, I’d flatten the SD-config type-switch into named per-provider validation functions — that alone would cut the nesting depth from 7 to something closer to 4 without changing behavior.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 10 |
deeply_nested | 10 |
exit_heavy | 10 |
god_function | 10 |
long_function | 10 |
cyclic_hub | 1 |
hub_function | 1 |
stale_complex | 1 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
See more analyses with these patterns: complex_branching, cyclic_hub, deeply_nested, exit_heavy, god_function, hub_function, long_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/prometheus/prometheus
cd prometheus
git checkout ea954809ceafceb53ecfa295ab0947753929de9e
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →