Caddy Architecture: How It Actually Works
How to read this page
Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. The terms:
- Module (or cluster)
- A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
- Modularity (the Q number)
- A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
- Hub
- The most depended-upon symbol inside one module.
- Load-bearing symbols
- PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
- Arrows and their numbers
- How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
- Dependency cycle
- File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
- Mutually recursive symbols
- Functions that call each other, usually the natural shape of parsers and tree-walking code.
Caddy is a web server built as a module system: a small core that parses
config, loads modules, and swaps configurations at runtime, with the HTTP
server, reverse proxy, and TLS automation all plugged in as modules. The
graph at 947087c shows that split directly. Symvanta detects 46 functional
modules at modularity Q=0.79, and the biggest clusters are the config
machinery itself: the Caddyfile dispenser (513 symbols), the placeholder
replacer (496), and the module registry and loader (356 and 311).
That is the structural signal: in most servers the request path dominates the
map, and in Caddy the config path does. RegisterModule, LoadModule, and
the Val token dispenser rank in the PageRank top 10 because every plugin
in the repo goes through them. The mutually recursive symbols tell the same
story: the largest group is the config-swap cycle in caddy.go
(changeConfig, run, provisionContext, finishSettingUp).
Module map
The diagram shows the 10 largest of the 46 detected modules, with edges
weighted by how many calls cross between them. The biggest holds 513 symbols
and its hub is Val, the token accessor on the Caddyfile dispenser
(caddyconfig/caddyfile/dispenser.go): every directive in every plugin reads
its arguments through it. Module names are checked by hand against the files
their members live in; the symbol counts, hubs, and edge weights are what the
graph computed.
Where to start reading
These are the most depended-upon symbols by PageRank over the call graph.
Three entries from the raw ranking are left out below: NewTester and
initServer belong to the integration test harness in caddytest, and
filtered is a zap field-encoder internal from the log filters. They rank
high without pointing anywhere useful.
LogValRegisterModuleLoadModuleNewContextprovisionHeaderAliasAllowlistdetermineTrustedProxyallTokens
Key subsystems
Caddyfile dispenser
Val, Next, NextArg, and ArgErr on the dispenser
(caddyconfig/caddyfile/dispenser.go): the token reader every Caddyfile
directive parses itself with. 513 symbols, the largest cluster in the repo,
and its edges fan out to the httpcaddyfile adapter, the registry, and the
core.
Placeholder replacer
NewReplacer and the {placeholder} substitution engine from replacer.go.
496 symbols. Placeholders reach into every layer, so this cluster talks to
ten others, with its heaviest edge into the HTTP server internals.
Core logging and network addresses
The caddy package's shared surface: the Log global logger accessor
(logging.go), network address parsing (JoinNetworkAddress,
ParseNetworkAddressWithDefaults), and config adapters. 445 symbols.
Module and directive registry
RegisterModule (modules.go) plus RegisterDirective and
RegisterHandlerDirective: the init-time registry every plugin registers
into. 356 symbols.
LoadModule and provisioning
Context.LoadModule and LoadModuleByID (context.go): the reflection
walk that turns raw JSON config into provisioned module instances. 311
symbols, and its heaviest edge runs into the core cluster (61 calls).
Context and metrics
NewContext (context.go:65) with the metrics registry and the
instrumented-route wrappers. 210 symbols.
Shared infrastructure
The rest of the top 10: the integration test harness (206 symbols, hub
NewTester), HTTP server internals (195, hub
provisionHeaderAliasAllowlist from modules/caddyhttp/server.go), the
httpcaddyfile adapter (166, hub Name), and the reverse proxy upstreams
(164, whose hub resetDynamicHosts is a test helper, which is what a hub
looks like when the tests drive every upstream path).
Canonical request flow
The sequence worth reading first is a config apply: what happens when a new
configuration reaches the admin API. Take the config lock, decode and diff
the JSON, provision a fresh Context, load every module the config references,
start the apps, then finish setting up the admin endpoint and config loaders.
Every step below is a call edge in the config-swap cycle in caddy.go, in
the order the work happens.
changeConfig (caddy.go:158)unsyncedDecodeAndRun (caddy.go:337)run (caddy.go:419)provisionContext (caddy.go:484)Context.LoadModuleLoadModuleByIDfinishSettingUp
Health signals
Symvanta detected 0 dependency cycles across 46 modules (modularity Q=0.79). 6 sets of mutually recursive symbols were also detected, the largest being caddy (9 symbols).
Auto-generated by Symvanta from the public repo caddyserver/caddy at commit 947087c , licensed Apache-2.0 .
Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).