Caching Architecture
Every page load traverses up to four cache tiers. Each tier eliminates a different class of work: browser I/O, edge CDN round-trips, per-section rendering, and Shopify origin fetches.
BROWSER CLOUDFLARE EDGE ORIGIN─────── ─────────────── ──────1. SW Prefetch ─miss─▶ 2. Dispatch CDN ─miss─▶ 3. Section Cache ─miss─▶ render (stale-while- (full-page, (per-section, revalidate) anonymous GET) input-hashed) │ 4. Origin Proxy (fallback) (Shopify HTML, 5m TTL)Layer 1 — Service Worker prefetch cache
Section titled “Layer 1 — Service Worker prefetch cache”Registered at /lk/sw.js, versioned per deploy (lk-prefetch-{deployVersion}).
A companion inline script observes viewport links via IntersectionObserver (200 px margin) and batches prefetch requests through the SW. Two-tier freshness:
| Age | Behaviour |
|---|---|
| < 5 min | Serve cached, revalidate in background |
| 5–30 min | Serve stale, network fetch queued |
| > 30 min | Entry expired, fetch fresh |
Prefetch strategy — deterministic URLs extracted at render time (zero extra KV calls):
- Index pages: collection URLs + first 4 products per collection
- Collection pages: first 6 product URLs + next page
- Blog pages: first 6 article URLs
- Max 10 URLs per page
Header: X-LK-SW: hit / stale / miss
Bypass: ?lk-fresh=1 skips SW entirely. Paths like /cart, /account, /checkout are never prefetched.
Invalidation: navigator.serviceWorker.controller.postMessage({type:'INVALIDATE'}) — or via window.lk.invalidate.sw() in debug mode. Old caches are cleaned on SW activation.
Key files: src/lib/sw-script.ts, src/lib/prefetch.ts
Layer 2 — Dispatch CDN cache
Section titled “Layer 2 — Dispatch CDN cache”Full-page caching at the edge via Cloudflare Cache API. Only anonymous GET requests to renderable paths are eligible.
Cache key:
{origin}{path}{search}?__v={variantId}&__dv={deployVersion}The __v parameter isolates AB variants; __dv isolates deploy versions.
Eligibility rules:
- GET only — POST/PUT always bypass
- No
cart=*cookie (cart cookie = personalized = uncacheable) - Not
/api/*,/__dev/*, or debug mode - Not immutable assets (
/assets/*-{hash}.*— cached separately withmax-age=31536000)
TTL: 24 hours (s-maxage=86400). Set-Cookie headers are stripped before storage.
Header: X-Cache: HIT / MISS / BYPASS
Invalidation: POST /api/cache/purge with Authorization: Bearer {token} + { paths: ["/products/snowboard"] }. Also forwards to the theme worker for section cache + KV eviction.
Key files: packages/cf-client-dispatch/src/worker.ts
Layer 3 — Section cache
Section titled “Layer 3 — Section cache”Per-section caching using content-addressable keys. Static sections are cached; dynamic sections (cart, customer) always render fresh.
Cache key:
https://section-cache/{slug}/{sectionType}/{inputHash}The input hash (FNV-1a 32-bit → 8-char hex) covers:
- Section settings (sorted JSON)
- Resource handle (product/collection slug)
- Locale
- Deploy version
- Market handle (for localized pricing)
This is self-invalidating — changing any input changes the hash, producing a new cache key.
TTL: 24 hours. Writes are fire-and-forget (don’t block the response).
Debug attributes: When debugMode=true, section wrappers include data-lk-section="{type}" and data-lk-cached="true".
Header: X-LK-Sections: {cachedCount}/{totalCount} (e.g. 7/9)
Bypass: ?lk-fresh=1 (requires debug mode) or DISABLE_SECTION_CACHE=true env var.
Key files: src/render/section-cache.ts, src/render/page-renderer.ts
Layer 4 — Origin proxy cache
Section titled “Layer 4 — Origin proxy cache”Fallback for templates not yet transpiled. Fetches Shopify-rendered HTML, caches it briefly so the actual transpiled page can replace it.
TTL: 5 minutes (s-maxage=300). Only 200 OK HTML responses are cached.
Header: X-LK-Cache: proxy-hit / proxy-miss
Bypass: skipCache=true (used by debug fresh mode).
Key files: src/middleware/cached-proxy.ts
Response headers
Section titled “Response headers”| Header | Source | Values |
|---|---|---|
X-LK-SW | Service Worker | hit, stale, miss |
X-Cache | Dispatch CDN | HIT, MISS, BYPASS |
X-LK-Sections | Section cache | {cached}/{total} |
X-LK-Cache | Theme worker | render, proxy-hit, proxy-miss, origin |
X-Rendered-By | Theme worker | layerkick, layerkick-proxy |
Debug mode
Section titled “Debug mode”Activate with ?lk-debug=1 (sets a 1-day cookie). Adds a console script and window.lk API:
window.lk.fresh() // Reload with ?lk-fresh=1 (bypasses all caches)window.lk.invalidate.sw() // Clear SW prefetch cachewindow.lk.invalidate.cdn(paths) // Purge CDN cache for pathswindow.lk.sections() // List sections with cache statuswindow.lk.off() // Disable debug modeConsole output: [LK] render | sections 7/9 | deploy: abc123
Disable with ?lk-debug=0 (clears cookie).
Cache bypass — ?lk-fresh=1
Section titled “Cache bypass — ?lk-fresh=1”Requires debug mode active. Bypasses every cache layer:
| Layer | Effect |
|---|---|
| SW Prefetch | Skipped entirely |
| Dispatch CDN | Returns X-Cache: BYPASS, sends X-LK-Fresh: 1 to theme worker |
| Section Cache | sectionCache set to undefined — all sections render fresh |
| Origin Proxy | skipCache=true — fetches from Shopify origin |
Cache invalidation
Section titled “Cache invalidation”Worker-level purge (/api/cache/purge)
Section titled “Worker-level purge (/api/cache/purge)”Purges the dispatch worker’s Cache API entries and forwards to the theme worker for section cache + KV eviction. This does NOT purge the Cloudflare CDN zone cache.
Webhook-driven: Shopify webhooks → cf-trk → POST /api/cache/purge with bearer token.
POST /api/cache/purgeAuthorization: Bearer {CACHE_PURGE_TOKEN}Content-Type: application/json
{ "paths": ["/products/snowboard", "/collections/all"], "keys": ["osc:product:snowboard"]}paths— evicts dispatch CDN cache entries (Cache API delete by URL)keys— evicts in-memory KV cache entries (next KV read will fetch fresh)
Cloudflare CDN zone purge (CF API)
Section titled “Cloudflare CDN zone purge (CF API)”The Cloudflare CDN edge also caches responses (with s-maxage=86400 TTL). This is a separate cache from the worker Cache API. After deploying or fixing a production issue, you must also purge the CDN zone cache to ensure users get fresh content immediately.
# Purge by hostnamecurl -X POST "https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"hosts":["example.layerkick.com"]}'
# Or purge everything in the zonecurl -X POST "https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -d '{"purge_everything":true}'Alternatively, purge from the Cloudflare Dashboard → Caching → Configuration → Purge Everything (or Custom Purge by hostname).
Self-invalidation
Section titled “Self-invalidation”Section cache keys are content-addressed — any setting change produces a new hash, so stale entries are never served (they expire via TTL).
SW invalidation
Section titled “SW invalidation”Deploy activates new SW version → old lk-prefetch-* caches deleted.
Key files
Section titled “Key files”| Layer | File |
|---|---|
| SW Prefetch | packages/cf-client-site/src/lib/sw-script.ts |
| Prefetch URLs | packages/cf-client-site/src/lib/prefetch.ts |
| Dispatch CDN | packages/cf-client-dispatch/src/worker.ts |
| Section Cache | packages/cf-client-site/src/render/section-cache.ts |
| Page Renderer | packages/cf-client-site/src/render/page-renderer.ts |
| Origin Proxy | packages/cf-client-site/src/middleware/cached-proxy.ts |
| Debug Mode | packages/cf-client-site/src/lib/debug.ts |
| Cache Purge | packages/cf-client-site/src/worker.ts (/api/cache/purge) |