moby/moby's daemon init carries the highest activity risk — 5 hotspots to fix first

An analysis of moby/moby finds NewDaemon in daemon/daemon.go as the top activity-risk function, with a cyclomatic complexity of 198 and 5 commits touching it in the last 30 days.

Stephen Collins ·
Generated by hotspots · free & open source
pip
$ pip install hotspots-cli
Activity Risk20.38Low
Hottest FunctionNewDaemon

Antipatterns Detected

exit_heavy10god_function10long_function10complex_branching8deeply_nested5stale_complex2hub_function1

Run this on your own codebase

See if your own repo has a NewDaemon-style hotspot — run this in any local git repo:

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in moby?

A god function is one that takes on too many responsibilities at once — parsing input, validating state, coordinating multiple subsystems, and handling cleanup all in a single body — which shows up as high cyclomatic complexity combined with high fan-out to other functions. In moby, all 10 of the functions I flagged in the top hotspots carry this pattern, with `NewDaemon` alone reaching a fan-out of 143. The practical cost is that no single engineer can hold the full set of responsibilities in their head during review, and a change intended for one concern (say, temp-directory setup) sits in the same function as unrelated concerns (registry service init, runtime configuration) with no isolation between them.

How do I reduce cyclomatic complexity in Go?

The most direct technique is extract-method: pull cohesive blocks of sequential logic — especially ones already separated by comments or blank lines, like the platform-specific branches in `NewDaemon` — into their own named functions with explicit error returns. As a rule of thumb, I treat cyclomatic complexity above 30 as worth scheduling and above 100 as worth addressing before the next feature lands on top of it; `NewDaemon` at 198 and `Images` at 106 both clear that second bar. A concrete first step: take the three near-identical `WalkValues` filter-parsing closures in `Images` (daemon/images/image_list.go) and consolidate them into one parameterized helper — that alone removes duplicated branching without touching the rest of the function.

Is moby actively maintained?

Yes — `NewDaemon` in daemon/daemon.go shows 5 touches in the last 30 days and was last changed just 6 days before this analysis, and context-only functions like `postContainersCreate` and `start` also fall in the 'fire' quadrant with 1 and 3 touches in 30 days respectively. At the same time, four of the five top hotspots (`delete`, `Images`, `restore`, `ImageDelete`) sit in the 'debt' quadrant with zero touches in the last 30 days and between 122 and 320 days since last change. Both things are true at once: moby has active near-term development on core daemon paths and a large body of structurally complex code — 1,674 critical-band functions out of 7,667 total, with 3,465 of all functions landing in the 'debt' quadrant — that simply isn't being touched right now.

How do I reproduce this analysis?

The hotspots CLI is available on GitHub, and I ran it against moby/moby at commit 275015b. After checking out that commit, the exact command is `hotspots analyze . --mode snapshot --explain-patterns --force`, which works against any local git repository without additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines structural complexity (cyclomatic complexity, nesting depth, and fan-out) with recent commit frequency, so functions that are both hard to understand and actively changing score highest. A function with cyclomatic complexity 80 that hasn't been touched in two years scores lower than one with lower complexity but frequent recent edits, because the dormant function carries lower near-term regression risk despite looking more complex on paper. In this analysis, that's why `NewDaemon` (cyclomatic complexity 198, 5 touches in 30 days, activity-weighted risk 20.38) outranks `delete` in daemon/libnetwork/network.go (cyclomatic complexity 39, 0 touches in 30 days, activity-weighted risk 19.1) by only a narrow margin despite the large complexity gap between them — `delete`'s 320 days of dormancy keeps its score close rather than pushing it far down the list.

Out of 7,667 functions analyzed in moby/moby, NewDaemon in daemon/daemon.go tops the list with an activity-weighted risk of 20.38 — a cyclomatic complexity of 198, a max nesting depth of 7, and fan-out to 143 distinct functions, combined with 5 commits touching it in just the last 30 days and a change only 6 days ago. That is not a dormant god function sitting untouched in the codebase; it is being actively edited while carrying the highest structural load I found in the repo. Docker’s engine is a large, mature codebase — 1,674 of its functions land in the critical band — so I’d start this week’s review with the daemon bootstrap path, not because it looks messy in the abstract, but because it is both complex and live.

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

FunctionFileRiskCCNDFO
NewDaemondaemon/daemon.go20.41987143
deletedaemon/libnetwork/network.go19.139337
Imagesdaemon/images/image_list.go18.4106649
restoredaemon/daemon.go18.1446102
ImageDeletedaemon/images/image_delete.go17.653627

Codemod / Tooling Files in Results

One pair of context-only functions, GenerateContainerName and its sibling generateContainerNameRequestFromProto, live in extpoints/containernamegenerator/v0/protogen/wire.gen.go — a generated wire-protocol file, not hand-written application code. Generated code like this inflates function counts without reflecting real maintenance burden, so I’d exclude it going forward with { 'exclude': ['**/protogen/**', '**/wire.gen.go'] } in .hotspotsrc.json.

Triage Band Distribution
Fire222Debt3465Watch124OK3856

7,667 functions analyzed

The quadrant split tells the real story here: 222 functions sit in the ‘fire’ quadrant — complex and actively changing right now — against 3,465 in ‘debt’, meaning most of moby’s structural risk is sitting quiet rather than being actively edited. That’s a normal shape for a codebase this size, but it means prioritization has to be quadrant-aware. A function with high complexity and zero recent touches is not the same risk profile as one being edited five times in the last month.

Detected Antipatterns
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.
Complex Branching×8Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Stale Complex×2Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.

Across the top hotspots, every single one carries the god_function, long_function, and exit_heavy patterns — ten out of ten. That’s a consistent signature: large functions with many early returns and broad responsibility, exactly what shows up in daemon setup, image listing, and container restore paths in this codebase.

NewDaemon — daemon/daemon.go

NewDaemon
daemon/daemon.go
20.38
critical
CC 198
ND 7
FO 143
touches/30d 5

This is the constructor that wires up a running Docker daemon: registry service, root key limits, config verification, bridge network state, resolv.conf, remapped root UID/GID, temp directory resolution, runtimes, and image store selection all happen in this one function, based on the excerpt I reviewed. A cyclomatic complexity of 198 with a nesting depth of 7 means 198 independent paths through initialization logic, and a fan-out of 143 means this function is coupled to a large share of the codebase directly — any change to one of those 143 callees is a candidate for breaking daemon startup. The external signals show 51 total commits and 7 distinct authors in the last 90 days touching this file, with a bug-fix fraction of 0.1373 — not a red flag on its own, but consistent with a function that many people are actively modifying. With 5 touches in the last 30 days and a change just 6 days ago, this is squarely a live-risk function, not a backlog item. My recommendation: extract the platform-specific temp-directory and runtime-setup blocks (the Windows/non-Windows branch and setupRuntimes sequence are clearly delineated in the excerpt) into named helper functions with their own error returns — that alone would cut both the nesting depth and the number of paths a reviewer has to hold in their head during the next PR touching this file.

delete — daemon/libnetwork/network.go

delete
daemon/libnetwork/network.go
19.1
critical
CC 39
ND 3
FO 37
touches/30d 0

This is network teardown logic — force-delete handling, load-balancer sandbox cleanup, gossip cluster departure, and store cleanup, based on the excerpt. It carries a cyclomatic complexity of 39 and fan-out of 37, and the code itself contains a comment acknowledging the risk directly: errors before a certain point are recoverable, errors after are not, because there’s no safe way to reconstitute a load-balancer endpoint once removed. That’s a structural signature worth taking seriously on its own terms. This function has 0 touches in the last 30 days and hasn’t been changed in 320 days — it is debt, not fire. The blast radius is real (37 callees, a goto for early exit to a cleanup label) but the risk is latent: it will surface the next time someone has to touch network deletion, likely under time pressure. I’d treat this as a pre-emptive refactor candidate — split the recoverable-error checks from the point-of-no-return cleanup sequence into two functions — rather than an urgent fix.

Images — daemon/images/image_list.go

Images
daemon/images/image_list.go
18.39
critical
CC 106
ND 6
FO 49
touches/30d 0

Images handles the list-images API path: filter validation, three separate WalkValues closures for the before/until/since date filters, dangling-image selection, and a per-image summary loop with container cross-referencing. A cyclomatic complexity of 106 with nesting depth 6 is high for a read-path function, and the three repeated filter-parsing closures are a clear extract-method opportunity — each follows the same shape (parse value, resolve to oldest/newest image, handle error) and could become a single parameterized helper. This function has 0 touches in the last 30 days and was last changed 122 days ago, so it sits in the debt quadrant: no one is actively editing it right now, but the next engineer who has to add a new filter type will inherit all 106 paths at once. Fan-out of 49 also means this function’s list-filtering logic isn’t isolated — it depends on image store internals and container state directly.

restore — daemon/daemon.go

restore
daemon/daemon.go
18.09
critical
CC 44
ND 6
FO 102
touches/30d 0

restore is the container-restoration path run at daemon startup, and it’s explicitly concurrent: goroutines launched per-container, a semaphore-limited parallelism model (adjustParallelLimit, sem.Acquire/Release), a shared sync.WaitGroup, and a mutex-guarded map for tracking containers to remove. Fan-out of 102 is the second-highest in this list, and nesting depth of 6 combined with per-goroutine error handling (log-and-continue rather than propagate, based on the excerpt) means the execution paths are not just numerous but concurrent — channel and goroutine coordination bugs here wouldn’t necessarily show up as a simple return-path error, which structural metrics alone can’t fully capture. This function shares a file with NewDaemon and shows the same 51 total commits / 7 authors in 90 days at the file level, but at the function level it has 0 touches in the last 30 days and hasn’t changed in 157 days — so despite living beside the hottest function in the repo, restore itself is dormant debt right now. Given the goroutine fan-out, I’d prioritize adding concurrency-specific tests (race detector runs against the parallel restore path) before the next refactor, not just splitting the function by line count.

ImageDelete — daemon/images/image_delete.go

ImageDelete
daemon/images/image_delete.go
17.64
critical
CC 53
ND 6
FO 27
touches/30d 0

ImageDelete handles image removal: platform selection, reference resolution, an inline using closure to check container-image association, and conditional untagging logic based on force/prune flags and whether the reference is a digest or tag. Cyclomatic complexity of 53 and nesting depth 6 reflect the branching around forced-versus-safe deletion and canonical-versus-tag reference handling. The stale_complex pattern is notable here alongside a bug-fix fraction of 0.5 at the file level (2 total commits, 1 in the last 90 days) — a small sample, so I wouldn’t read too much into it, but it does mean half of the recorded history on this file was bug-fix related. Combined with 320 days since this function was last changed, this is tied with delete for the stalest function in the top five: high blast radius, no recent attention. If image deletion logic needs to change for any reason — new platform support, new retention policy — this function will absorb that change across all 53 paths at once.

A few functions in the context data are worth a mention without full sections: postContainersCreate (daemon/server/router/container/container_routes.go, cyclomatic complexity 121, activity risk 17.42, touched once in the last 30 days) and start in daemon/command/daemon.go (cyclomatic complexity 79, fan-out 103, activity risk 16.98, 3 touches in 30 days) are both ‘fire’ quadrant and sit just below the top five — worth watching on the next pass. ServiceSpecToGRPC in daemon/cluster/convert/service.go is also ‘fire’ with a cyclomatic complexity of 77 despite a comparatively low fan-out of 17, suggesting its complexity is concentrated in branching rather than coupling.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
exit_heavy10
god_function10
long_function10
complex_branching8
deeply_nested5
stale_complex2
hub_function1

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, deeply_nested, exit_heavy, god_function, hub_function, long_function, stale_complex.

Reproduce This Analysis

git clone https://github.com/moby/moby
cd moby
git checkout 275015b0e7d1e17941df3dad1503263d653588e5
hotspots analyze . --mode snapshot --explain-patterns --force

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 →

Was this useful? Let me know →

Related Analyses