kodebeat / papers / flagship

Full-page caching on infrastructure you control

A Redis-backed caching reverse proxy in Go.

A reverse proxy with full-page caching in Redis, covering the operational cases that separate a working cache from a demonstration: stampede prevention, stale content served for up to ten seconds to absorb a thundering herd, a circuit breaker that bypasses Redis when Redis is unavailable, and full HTTP cache-header semantics. Load balancing, ACME, HTTP/2, tracing and Prometheus come with it.

The problem

A publisher's site runs behind six caching proxies on its own hardware; data residency rules keep the traffic off a CDN. Each proxy keeps its own on-disk cache. After a deploy all six start cold, and the origin takes six times the misses it would take from one cache. An editor corrects a headline and the purge reaches five of the six; for the next hour one visitor in six still sees the old headline, and nobody can say which proxy is serving it.

Underneath the choice of tool sit the failure modes that make caching hard, and that a naive implementation gets wrong:

  • Cache stampede. A popular object expires. Every in-flight request misses simultaneously and all of them hit the origin at once, which can take down the origin that the cache existed to protect.
  • The cache becoming a dependency. Redis goes away. If the proxy treats a cache lookup as required, every request now fails, and an availability improvement has become an availability risk.
  • Header semantics. Vary, ETag, conditional requests, Cache-Control and Expires interacting correctly is a specification-shaped problem, and getting it wrong means serving one user's content to another.

The method

Redis as the cache, so the cache is shared. This is the architectural choice that distinguishes it. Multiple proxy instances share one cache: a cold start does not multiply origin load by the number of instances, and a PURGE takes effect fleet-wide because there is one copy to invalidate.

Handle the stampede explicitly.

  • Cache stampede prevention delays the invalidation request to the backend using an extra small random TTL of 5–10 seconds. Randomising expiry spreads what would otherwise be a synchronised miss across a window.
  • Serving stale content, for at most ten seconds, so requests arriving during a revalidation are answered from the old copy rather than queued behind the origin.
  • Collapsed forwarding, an opt-in per upstream (collapsed_forwarding, off by default), coalesces concurrent identical GET and HEAD fetches so a miss for one resource reaches the origin once rather than once per waiting request.

The first two are bounded: a random window of a few seconds, a stale window of ten. A cache that served stale content indefinitely would trade one problem for a worse one.

A cache circuit breaker: bypass Redis when it is not available. A cache is an optimisation, and an optimisation must not become a hard dependency. When Redis is unhealthy the proxy stops trying and forwards to the origin: slower, and still serving. A proxy that treats every cache lookup as required converts a Redis outage into a total outage.

Respect the HTTP caching specification rather than approximating it. Vary, ETag, Cache-Control and Expires are honoured. ETags are generated non-weak, 304 Not Modified is handled, and If-Modified-Since, If-Unmodified-Since, If-None-Match and If-Match are all managed.

Cache the upstream DNS resolution, so the proxy is not paying a resolver round trip on every request.

YAML, with per-domain overrides. Global settings, overridden and fine-tuned per domain, in a format everyone already reads:

server:
  port: { http: "80", https: "443" }
  upstream:
    host: ~
    endpoints: [127.0.0.1]
    http_to_https: true
cache:
  hosts: [localhost:6379]
domains:
  example_com:
    server: { upstream: { host: example.com } }
  example_org:
    server: { upstream: { host: example.org } }

No new language to learn, which is the explicit contrast with VCL.

The headline results

  • Tests.
  • Adoption.
  • Release policy is published

The full results, with the tables and the reasoning behind each figure, are in the PDF.

What is in the PDF

  • The problem — what goes unanswered without it, and who notices first.
  • Why the obvious alternative falls short — stated plainly, including where it is the better choice.
  • How it works — the method, not a feature list.
  • Concrete use cases — with console output quoted from the repository, never reconstructed.
  • The methodology behind any number it emits — every term shown, so the figure survives a question.
  • What it deliberately does not do — the section most papers leave out.

Part of the catalogue.

Get the PDF

This paper already selected; the download starts when you send the form. No follow-up sequence.

Get the PDF