Caddy guide
Build Caddy with the weir handler, configure it in a Caddyfile, and purge through the admin API.
The Caddy module puts a weir directive in front of reverse_proxy. Its import path is github.com/AshwinSathian/weir/caddy and its Go package name is weircaddy. It is built against Caddy v2.11.7.
Build
xcaddy build --with github.com/AshwinSathian/weir@v1.0.0 --with github.com/AshwinSathian/weir/caddy@v1.0.0
./caddy list-modules | grep weir
Pin both modules to the same version. The four Weir modules are released together, and the Caddy module is tested only against the root module at its own version. Upgrade by rebuilding.
A site
{
grace_period 15s
servers {
timeouts {
read_body 10s
}
}
}
example.com {
request_body {
max_size 10MB
}
weir {
name site-a
max_bytes 512MiB
snapshot_dir /var/lib/caddy/weir
}
reverse_proxy app:8080 {
header_up -X-Forwarded-For
}
}
nameis required. It is the store identity across reloads, thenamemetric label, the admin URL segment and the snapshot file name, so pick it once and keep it.request_body { max_size }and the serverread_bodytimeout are required on Weir routes (FR-LIM-7, T-39). Weir bounds origin concurrency, but a slow or oversized upload still holds a slot from the upload pool until it finishes. Size the limit to your largest legitimate upload.snapshot_dirmust be absolute, owned by the Caddy user and not writable by group or others. Weir creates it with mode 0700 and refuses a shared one (a planted snapshot would be served as trusted cache content). The file is<snapshot_dir>/<name>.weir, written when the store closes and read at the next start. Withoutsnapshot_direvery restart is a cold cache.- The snapshot is written during shutdown, so give Caddy time:
grace_periodis the time Caddy waits for requests to drain before it stops, and the engine close and the store close can each take up to the 5 s snapshot timeout after that, so about 10 s more. A service manager that kills the process sooner (TimeoutStopSecin systemd,terminationGracePeriodSecondsin Kubernetes) loses the snapshot. The packagedcaddy.servicesetsTimeoutStopSec=5s, which is too short: raise it abovegrace_periodplus 10 s with a drop-in. Withoutgrace_periodCaddy waits for connections indefinitely, so the unit's kill timer is the only bound. A reload that replaces a large store waits about as long for the old config to stop (08 §3). snapshot_diralso holds<name>.weir.keygen, a 32-byte record of the forwarding settings the snapshot was stored under. Do not delete it as debris: without it the next start discards the snapshot. A start after a change to theforwardblock discards it too, on purpose (08 §3).
Directive reference
Every key is optional except name. Unset keys take the engine default from the configuration reference.
| Key | Meaning |
|---|---|
name | Required. 1 to 64 characters of A-Z a-z 0-9 . _ -. Store identity across reloads, metrics label, admin URL segment, snapshot file name |
max_bytes | Memory store size, for example 512MiB. At least 160 MiB when set. Unset: sized from GOMEMLIMIT |
snapshot_dir | Absolute directory for the snapshot file. Unset: every restart is a cold cache |
multi_host | Set it when the site serves more than one host. Turns on the per-host limiter cap and store quota at 25% |
store valkey { ... } | Shared store for several nodes: addrs, username, password, tls, cluster, prefix |
key { ... } | query_drop, query_keep, query_sort, normalize_path, headers, cookies, accept_encoding |
forward { allow ... } | Headers the origin may see unkeyed. The mode is always strict: ForwardAll has no Caddy form |
bypass { ... } | cookies, headers: requests that skip the cache |
limiter { ... } | max_concurrent, max_queue, max_queue_wait, max_per_partition |
stale { ... } | while_revalidate, if_error: operator defaults, off unless set |
experiments { ... } | A/B bucketing; see the experiments guide |
A fuller example:
example.com {
weir {
name site-a
max_bytes 512MiB
key {
query_drop utm_* fbclid gclid
query_sort
headers Accept-Language
cookies currency
accept_encoding br gzip
}
bypass {
cookies session_id
}
limiter {
max_concurrent 128
}
stale {
if_error 5m
}
}
reverse_proxy app:8080
}
Each key may appear once per block, so list values go on one line. A repeated key is an error. The JSON config uses the same names in snake_case.
Where weir sits among handlers
Caddy sorts directives inside a site block by a fixed order, not by where you wrote them. In v2.11.7 weir sits directly before reverse_proxy and after encode, request_header, basic_auth, forward_auth, handle and route. Anything that must see a request before the cache does is therefore already outside it. Anything you want inside it needs a route block with an explicit order. If reverse_proxy is inside handle { } or route { }, put weir in the same block.
-
Auth before
weir.basic_auth,forward_authand any identity handler must run before the cache, and a route that needs per-user responses should list its session cookie or header underbypass { cookies ... }(orheaders) or rely onAuthorizationhandling (FR-STO-5). The handler behind Weir sees the creating request's context values (T-45), so a variable that an auth handler put there can shape a response that is then stored and shared. Weir cannot key what it cannot see. Derive per-user output only from the forwarded request, or keep the route out of the cache (06 R-6). -
rate_limitbeforeweir. This is the answer to distinct-path floods (06 R-2). The third-partycaddy-ratelimitmodule registers its directive with the default orderbefore basic_auth(itscaddyfile.goonmaster, read 2026-10-10; check the version you build), so it is outsideweirwithout any setting. Do not move it with a globalorderdirective to afterweir: a limiter behind the cache never sees hits and so cannot count them, and a flood of misses would be limited only after Weir had queued them. Verify the result on your build, not on this page:caddy adapt --config Caddyfile --pretty | grep -n '"handler"'The
rate_limithandler must be listed aboveweir, andreverse_proxybelow it. A site that usesroute { }follows the order written inside the block. -
encode. Preferred: let the origin compress and leaveencodeout of cached routes. If the origin cannot, putencodeoutsideweir(the default order does this, so a plainencodeline works):example.com { encode zstd gzip weir { name site-a } reverse_proxy app:8080 }Weir sends the origin the normalized
Accept-Encoding(one coding fromaccept_encoding, defaultgzip), so an origin that can compress will return compressed bodies and Weir stores those, one per bucket. Weir caches uncompressed bytes only when the origin does not compress, and thenencodecompresses every response, hits included, which costs CPU on each hit. The other arrangement putsencodeinside, betweenweirandreverse_proxy, so Weir stores one compressed variant perAccept-Encodingbucket:example.com { route { weir { name site-a key { accept_encoding zstd gzip } } encode zstd gzip reverse_proxy app:8080 } }Use it only when the compression CPU matters. Set
accept_encodingto the encodingsencodeproduces (the default isgziponly). Weir sendsencodethe single preferred listed coding, oridentitywhen none qualifies, so an unlistedbris never used andAccept-Encodingreaches the entry key only through the origin'sVary(01 §5.2.3). ListAccept-Encodingunderkey { headers }if storability differs by coding.
Purges, modes and stats
Start with the safe setup, then the commands.
Purge, mode and stats are on Caddy's admin endpoint (admin.api.weir); no purge route is exposed on site listeners. The admin API has no authentication of its own. It listens on localhost:2019, which every local user and every container sharing the network namespace can reach; Caddy always checks the Host header, and checks Origin only when the request carries one. On a shared host listen on a unix socket instead (admin unix//run/caddy/admin.sock|0660 in the global options) and give the group only to the operators. If you must call it from another machine, put it behind an SSH tunnel or a reverse proxy that authenticates (it must send a Host that matches the admin origins list, or Caddy answers 403), and never expose it unauthenticated (T-26): anyone who can reach it can flush the cache or switch the engine into bypass.
## Before you go live
1. `caddy validate --config Caddyfile --adapter caddyfile` on the final Caddyfile (it runs `Provision`, so a bad `weir` block fails here). `validate` builds the store for real: it creates `snapshot_dir`, loads and rewrites the snapshot and its `.keygen` record, and a changed `forward` block discards the snapshot. On a site with a `store valkey` block it also runs the key-generation check against the real server (08 §3): if `forward` changed, `validate` writes the hard epoch and replaces the record, which spends the rollout's early purge on a command that applied nothing, so validate against a scratch Valkey (a copy of the Caddyfile with other `addrs`) or accept that the cache is purged and purge once more after the rollout. Run it as the user Caddy runs as (as root it creates a root-owned directory that Caddy then refuses), and on a node that is already serving, validate a copy whose `snapshot_dir` is a scratch directory.
2. `caddy adapt --pretty` and check the handler order (7.3).
3. One request twice with `curl -si`: the second shows `Cache-Status: Weir; hit`.
4. A purge on a test URL (7.7), wait 2 s (a soft purge on the memory store can land up to 1 s late), then a request showing `fwd=stale` or `fwd=uri-miss`.
5. Stop the service and confirm the snapshot file `<snapshot_dir>/<name>.weir` exists (the next start reads and removes it, so look while Caddy is stopped). Start it again: the first request for a URL cached before the stop shows `fwd=stale`, not `hit` and not `fwd=uri-miss`. Loaded entries are revalidated once before use (FR-SNP-2), which costs a 304 when the origin sends a validator and a full fetch when it does not; the second request is a hit.
## Several nodes
One Caddy node per cache with the memory store. For several nodes, add a `store valkey` block and follow the runbook's [multi-node checklist](https://github.com/AshwinSathian/weir/blob/main/docs/runbook.md#8-deploying-on-caddy-several-nodes-one-valkey): one prefix per cache, the same `forward`, `key` and `stale` settings on every node, an eviction policy that cannot evict purge records, and clocks in step.
## Limits
> [!LIMITS]
> The directive order (`weir` directly before `reverse_proxy`) is registered for Caddy v2.11.7; check `caddy adapt --pretty` on your build. `caddy validate` builds the store for real: it creates `snapshot_dir`, rewrites the snapshot, and on a Valkey site can spend a pending purge, so validate against a scratch server. There is no purge route on site listeners, on purpose. `ForwardAll` cannot be configured from Caddy.