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
	}
}
  • name is required. It is the store identity across reloads, the name metric label, the admin URL segment and the snapshot file name, so pick it once and keep it.
  • request_body { max_size } and the server read_body timeout 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_dir must 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. Without snapshot_dir every restart is a cold cache.
  • The snapshot is written during shutdown, so give Caddy time: grace_period is 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 (TimeoutStopSec in systemd, terminationGracePeriodSeconds in Kubernetes) loses the snapshot. The packaged caddy.service sets TimeoutStopSec=5s, which is too short: raise it above grace_period plus 10 s with a drop-in. Without grace_period Caddy 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_dir also 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 the forward block 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.

KeyMeaning
nameRequired. 1 to 64 characters of A-Z a-z 0-9 . _ -. Store identity across reloads, metrics label, admin URL segment, snapshot file name
max_bytesMemory store size, for example 512MiB. At least 160 MiB when set. Unset: sized from GOMEMLIMIT
snapshot_dirAbsolute directory for the snapshot file. Unset: every restart is a cold cache
multi_hostSet 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_auth and any identity handler must run before the cache, and a route that needs per-user responses should list its session cookie or header under bypass { cookies ... } (or headers) or rely on Authorization handling (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_limit before weir. This is the answer to distinct-path floods (06 R-2). The third-party caddy-ratelimit module registers its directive with the default order before basic_auth (its caddyfile.go on master, read 2026-10-10; check the version you build), so it is outside weir without any setting. Do not move it with a global order directive to after weir: 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_limit handler must be listed above weir, and reverse_proxy below it. A site that uses route { } follows the order written inside the block.

  • encode. Preferred: let the origin compress and leave encode out of cached routes. If the origin cannot, put encode outside weir (the default order does this, so a plain encode line works):

    example.com {
    	encode zstd gzip
    	weir {
    		name site-a
    	}
    	reverse_proxy app:8080
    }

    Weir sends the origin the normalized Accept-Encoding (one coding from accept_encoding, default gzip), 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 then encode compresses every response, hits included, which costs CPU on each hit. The other arrangement puts encode inside, between weir and reverse_proxy, so Weir stores one compressed variant per Accept-Encoding bucket:

    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_encoding to the encodings encode produces (the default is gzip only). Weir sends encode the single preferred listed coding, or identity when none qualifies, so an unlisted br is never used and Accept-Encoding reaches the entry key only through the origin's Vary (01 §5.2.3). List Accept-Encoding under key { 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.