Skip to content

Cache and invalidation

This page documents the cache-control story end-to-end.

Today: no-cache everywhere

Every asset response from MatrixHttpAssetEndpointActor carries:

cache-control: no-cache

Per index.ts:163:

typescript
const headers: Record<string, string> = {
  'content-type': MIME_TYPES[ext] ?? 'application/octet-stream',
  'content-length': String(stat.size),
  'cache-control': 'no-cache',
};

no-cache does not mean "do not cache." It means "the cache may store this, but must revalidate before serving from cache." A 304 Not Modified flow is supported; the browser sends If-None-Match and the server responds 304 if the ETag matches.

Note: The current asset endpoint does not implement ETag/Last-Modified validation. If-None-Match requests succeed at the cache check level (the server doesn't refuse) but always return the full body. Adding proper conditional-request handling is part of the cache-tightening target state.

Why no-cache today

Pre-launch development is fast-moving. Tightening caching introduces a class of "I deployed a fix but the browser still shows the old version" bugs. no-cache is the safe default while the deployment cadence is high.

Target state

Two-tier caching:

Asset classCache-control
Hashed asset (*.abc123.js, *.def456.css)public, max-age=31536000, immutable
index.html, manifest.jsonno-cache (or short max-age)
API responses (/api/*)no-store

Hashed assets are content-addressed by name; their content cannot change without their URL changing, so an aggressive immutable directive is correct. Browsers and CDNs cache them indefinitely.

index.html references the hashed asset URLs. When a new build ships, index.html references new asset URLs; the browser revalidates index.html (short cache), gets the new version, and fetches the new hashed assets.

Invalidation

In the target-state model:

  • Deploying a new package version produces a new build with new asset hashes.
  • The new index.html references the new hashed assets.
  • Browsers fetching index.html revalidate (short cache or no-cache).
  • They get the new HTML, then fetch the new asset URLs (cache miss, fresh fetch).
  • Old asset URLs in old caches are never refetched and eventually evict.

This is the standard "cache-busting via filename" pattern that every modern build tool produces.

CDN considerations

When a CDN sits between Caddy and the browser (HiveCast does not currently deploy with a CDN, but a future architecture could), the CDN respects cache-control directives. Hashed assets get aggressive CDN caching; index.html does not. CDN purges become unnecessary because nothing long-cached needs invalidating.

What doesn't help today

  • Adding Cache-Control: max-age=0 headers everywhere. Same as no-cache for our purposes; doesn't solve the underlying revalidation issue.
  • Setting must-revalidate. Combined with no-cache, redundant.
  • Versioning asset URLs by query string (/index.js?v=2). Hashed asset names are strictly better — the browser cache can store both versions side-by-side during transitions.

Browser cache when WebSocket disconnects

NATS WebSocket disconnect doesn't affect HTTP cache. Each is independent. The browser may notice the WS disconnect, attempt to reconnect, and during that period continue using cached HTML/JS. State refresh happens through NATS, not HTTP cache invalidation.

Asset endpoint relay limit

Unrelated to caching but worth mentioning here: assets >10 MB cannot be relayed (the MAX_RELAY_BODY_BYTES constant). Caching a 10 MB asset is fine once it has been fetched, but the gateway's relay layer is the bottleneck for the initial fetch.

See also

Source: projects/matrix-3/packages/system-gateway-http/src/index.ts:160-167.