Go library · net/http and Caddy · Apache-2.0

Shared HTTP caching in front of an origin that has to stay up.

Weir forwards the request it keyed, so a header the cache ignores cannot poison an entry. And a stampede cannot reach the origin faster than a budget you set.

$go get github.com/AshwinSathian/weir@v1.0.0
weirproxy in front of an origin
$ weirproxy -listen :8080 -origin http://localhost:9000
$ curl -si localhost:8080/
HTTP/1.1 200 OK
Cache-Control: max-age=60
Cache-Status: Weir; fwd=uri-miss; fwd-status=200; stored

hello from the origin
# 13 seconds later
$ curl -si localhost:8080/
HTTP/1.1 200 OK
Age: 13
Cache-Control: max-age=60
Cache-Status: Weir; hit; ttl=43

hello from the origin

Real output. The second request never left the process. Its ttl is 43 and not 47 because jitter took four seconds off this entry, so entries stored together do not expire together.

What a weir does

Ten thousand entries expire within seconds of each other. The origin sees 32 requests at a time.

A weir is a low dam that regulates flow without stopping it. Every origin fetch holds a slot from a limiter, whatever caused it: a miss, a refresh, a purge, a cold start.

Demand against origin load during a synchronized expiryOn the left, demand rises to a sharp peak as 10,000 entries expire within seconds. On the right of the weir, origin fetches in flight stay flat at the limit of 32.Requests whose entry just expired10,000 keys, max-age=60, stored within 1.7 sOrigin fetches in flightpeak 32, the configured limitMaxConcurrent = 32weirtimeall 10,000 refetched, 0 client errors
The shape is a schematic. The numbers are measured: the synchronized-expiry scenario of make load on an Apple M4 Pro with Go 1.27.1, where the busiest second saw 1,485 origin calls. Method and limits
Failure modes

Five ways a cache takes its origin down

Each technique exists elsewhere. Weir puts them in one Go library with a written threat model and a test for each.

  1. 01

    Synchronized expiry

    Entries stored together expire together, and the origin gets the whole batch back at once.

    What Weir does. Jitter shortens each lifetime by up to 10%, and a hot entry is refreshed in the background shortly before it expires. Freshness

  2. 02

    Hot-key stampede

    One popular entry expires and every request waiting on it asks the origin.

    What Weir does. Requests for one key share one fetch. Followers wait at most 10 s, and a leader slower than 10 s stops collecting new followers. Coalescing

  3. 03

    Cross-key stampede

    A purge, a restart or a cold start makes thousands of different keys miss together. Coalescing cannot help, because no two requests share a key.

    What Weir does. Every fetch holds one of 64 slots, at most 16 per path. 1,024 more wait up to 2 s; the rest get stale content or a 503 with Retry-After. Origin protection

  4. 04

    Origin or storage outage

    The origin slows down, and every request pays the full timeout before failing.

    What Weir does. Stale-while-revalidate and stale-if-error serve what is stored, a circuit breaker stops calling a failing origin, and a 2 s negative entry answers repeats of a failure. Origin protection

  5. 05

    Poisoning and cache busting

    A header the key ignores changes the stored response for everyone, or random query strings turn every request into a miss.

    What Weir does. The origin receives only keyed, allowed and protocol headers, so there is no unkeyed input to poison with. A miss-rate detector flags a path whose requests have stopped hitting. The cache key

Try it

A main function, or a Caddyfile block

The zero weir.Config is valid, and name is the only setting the Caddy directive requires.

Go, with net/http

target, _ := url.Parse("http://localhost:9000") // scheme and host; a path is ignored
e, err := weir.New(weir.Config{})               // the zero Config is valid
if err != nil {
	log.Fatal(err)
}
srv := &http.Server{
	Addr:              ":8080",
	Handler:           weirhttp.Handler(e, &weirhttp.TransportOrigin{Target: target}),
	ReadHeaderTimeout: 10 * time.Second,
}
log.Fatal(srv.ListenAndServe()) // on shutdown: srv.Shutdown(ctx), then e.Close(ctx)

Caddy

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

Build Caddy with the module first. The quickstart has the xcaddy line and the output to expect at each step.

Non-goals

What Weir is not

  • A CDN. One process, one logical origin per engine.
  • A WAF or rate limiter. It bounds origin load. It does not decide which clients deserve service, so put a rate limiter in front for floods of distinct paths.
  • A private cache. It is always a shared cache in the RFC 9111 sense.
  • A cache for application data. It caches HTTP responses. For function results use singleflight or sturdyc.
  • A Varnish replacement with VCL. Configuration is a Go struct or a Caddyfile block. There is no scripting language.
  • A store for 206 responses. It answers single byte ranges from complete stored objects.
  • A prefix purge. Purge by URL, by RFC 9875 group, or everything.
  • A multi-node memory cache. Each node has its own memory store. Several nodes share a cache through the Valkey store.

If one of these is what you need, the comparison page says which project to use instead.