Cache invalidation for people who hate cache invalidation
Fine-grained invalidation is a graph problem you will lose. A generation counter is a single integer that cannot be subtly wrong — and for a blog, it costs you almost nothing.
There are two hard problems in computer science, and the joke has survived forty years because the second one is genuinely hard. The usual fix — track which pages depend on which rows — is correct until the day someone adds a widget that reads a table nobody mapped.
The cheap correct answer
Keep one integer. Every cached entry records the generation it was written in. Any write increments the integer. Lookups compare generations, and mismatched entries are dead.
type Cache struct {
mu sync.RWMutex
items map[string]*CacheEntry
gen uint64
}
func (c *Cache) Bump() {
c.mu.Lock()
c.gen++
c.items = make(map[string]*CacheEntry)
c.mu.Unlock()
}
Invalidation is O(1) and universally correct. The cost is that publishing one post discards every cached page. For a blog, that is a rounding error: the whole site re-warms in the time it takes a crawler to walk twenty URLs.
When it is the wrong tool
If a single write invalidates a working set larger than your origin can re-render in a few seconds, you need finer granularity. That is the point where you should probably be using a CDN with tag-based purging rather than hand-rolling dependency graphs.
Measure before you assume. A page that renders in 4 ms can be rebuilt 250 times per second per core. You need to be serving a lot of traffic before a full flush hurts more than the bug that a stale widget would cause.
Stale-while-revalidate is the real win
The generation counter handles correctness. Header policy handles latency:
Cache-Control: public, max-age=60, s-maxage=600, stale-while-revalidate=86400
Readers get an instant response from the edge or the browser; the origin refreshes in the background; the crawler sees fresh HTML within a minute. Combined with a strong ETag, repeat visitors transfer zero bytes for the document.
The rule
Cache the rendered artifact, not the data. Data caches need dependency tracking, invalidation events and careful key design. A rendered page is a value produced by a pure function — flush it and it comes back identical. That is a much smaller problem to be wrong about.