Skip to content

Cache Strategy

Purpose: Explain Lantern's multi-layered cache strategy for optimal performance and instant updates.


Overview

Lantern uses a three-tier cache strategy to balance performance with fresh content delivery:

  1. Cache Busting - Immutable assets with unique filenames
  2. Short TTL for HTML - 5-minute cache window for quick updates
  3. Emergency Purge - Manual purge script for critical situations

Layer 1: Cache Busting (Primary Strategy)

Implementation

Build-time filename generation with unique build IDs:

javascript
// vite.config.mjs
build: {
  rollupOptions: {
    output: {
      entryFileNames: `assets/[name]-${buildId}.[hash].js`,
      chunkFileNames: `assets/[name]-${buildId}.[hash].js`,
      assetFileNames: `assets/[name]-${buildId}.[hash][extname]`,
    },
  },
}

Build ID generation (scripts/generate-version.mjs):

  • Creates public/version.json with unique build ID
  • Build ID format: {timestamp}-{commit-sha-8}
  • Never cached by Cloudflare (Cache-Control: no-cache)

Result

  • JS/CSS assets: Cached forever (max-age=31536000, immutable)
  • New deployment: New filenames = automatic cache bypass
  • Zero stale assets: Changed files always have new URLs

Example:

Old: assets/main-20260120-abc12345.js
New: assets/main-20260122-def67890.js

Layer 2: Short TTL for HTML

Configuration

public/_headers:

/*.html
  Cache-Control: public, max-age=300, stale-while-revalidate=60, must-revalidate

/
  Cache-Control: public, max-age=300, stale-while-revalidate=60, must-revalidate
  
/index.html
  Cache-Control: public, max-age=300, stale-while-revalidate=60, must-revalidate

Behavior

  • max-age=300: HTML cached for 5 minutes at CDN edge
  • stale-while-revalidate=60: Serve stale content while fetching fresh (better UX)
  • must-revalidate: Force revalidation after expiry

Why 5 Minutes?

  • Performance: Reduces origin hits for high-traffic periods
  • Freshness: Users get updates within 5 minutes
  • Balance: Good middle ground between instant updates (0s) and long caching (hours)

Layer 3: Purge on Deploy (Automatic)

Implementation

GitHub Actions (.github/workflows/deploy-dev.yml, .github/workflows/deploy-prod.yml):

yaml
- name: Purge Cloudflare Cache
  run: |
    curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/purge_cache" \
      -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
      -H "Content-Type: application/json" \
      --data '{"files":["https://ourlantern.app/","https://ourlantern.app/index.html"]}'

What Gets Purged

Only HTML files:

  • / (root)
  • /index.html

Not purged:

  • JS/CSS assets (cache busting handles these)
  • Images/fonts (static, rarely change)
  • Service worker (already has no-cache headers)

Why Selective Purging?

  • Efficiency: Only purge what needs updating
  • Cost: Cloudflare has purge rate limits
  • Performance: Keep static assets cached globally

Layer 4: Emergency Full Purge (Manual)

When to Use

Use full purge ONLY when:

  • Cache busting isn't working (deployment issue)
  • Wrong content served globally
  • Critical bug requiring immediate flush
  • Something is "on fire"

DO NOT use for:

  • Normal deployments (automatic purge handles this)
  • Minor updates (5-minute TTL handles this)
  • Testing (use ?cacheBust=123 query param)

Usage

scripts/purge-cache.sh:

bash
# Purge dev environment
./scripts/purge-cache.sh dev

# Purge production
./scripts/purge-cache.sh prod

# Nuclear option - purge everything
./scripts/purge-cache.sh all

Prerequisites

Add to .env.local:

bash
CLOUDFLARE_ZONE_ID=your_zone_id_here
CLOUDFLARE_API_TOKEN=your_api_token_here

Get credentials:

  • Zone ID: Cloudflare Dashboard → Your Domain → Overview (right sidebar)
  • API Token: Cloudflare Dashboard → My Profile → API Tokens
    • Required permission: Zone.Cache Purge

What Gets Purged

Everything:

  • All HTML files
  • All JS/CSS assets
  • All images, fonts, icons
  • All cached API responses
  • Everything Cloudflare has cached

Impact

  • Users: Slower load times until cache rebuilds
  • Origin: Higher server load temporarily
  • Cloudflare: May trigger rate limits if done repeatedly

Cache Flow Diagram

┌─────────────────────────────────────────────────────────┐
│ 1. User visits ourlantern.app                           │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 2. Cloudflare CDN checks cache for index.html          │
│    • Fresh (< 5 min): Serve from cache                 │
│    • Stale (> 5 min): Fetch new, serve old while wait  │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 3. HTML references assets with build IDs               │
│    <script src="/assets/main-20260122-abc123.js">      │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 4. CDN checks cache for JS/CSS                         │
│    • Hit: Serve from cache (1 year max-age)            │
│    • Miss: Fetch from origin, cache for 1 year         │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 5. App loads and checks version.json                   │
│    • Same version: Continue normally                   │
│    • New version: Auto-reload app                      │
└─────────────────────────────────────────────────────────┘

Deployment Flow

┌─────────────────────────────────────────────────────────┐
│ 1. Developer pushes to main                            │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 2. CI/CD builds app                                     │
│    • Generates new build ID                            │
│    • Creates version.json                              │
│    • Builds assets with new filenames                  │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 3. Deploys to Cloudflare Pages                         │
│    • dist/ uploaded to CDN                             │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 4. Purges HTML from cache                              │
│    • / and /index.html only                            │
│    • Forces fresh HTML on next visit                   │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 5. Users get new version                               │
│    • Next visit: Fresh HTML (purged)                   │
│    • HTML loads new JS/CSS (new filenames)             │
│    • Old assets ignored (no references)                │
└─────────────────────────────────────────────────────────┘

Cache Headers Reference

Static Assets (JS/CSS/Images)

/assets/*
  Cache-Control: public, max-age=31536000, immutable
  • public: Can be cached by CDN
  • max-age=31536000: Cache for 1 year
  • immutable: Browser won't revalidate (trusts it never changes)

HTML Files

/*.html
  Cache-Control: public, max-age=300, stale-while-revalidate=60, must-revalidate
  • public: Can be cached by CDN
  • max-age=300: Cache for 5 minutes
  • stale-while-revalidate=60: Serve stale + fetch fresh in background
  • must-revalidate: Force check after expiry

Service Worker

/sw.js
  Cache-Control: no-cache, no-store, must-revalidate
  • no-cache: Always revalidate with origin
  • no-store: Never cache
  • must-revalidate: Force revalidation

Version File

/version.json
  Cache-Control: no-cache, no-store, must-revalidate

Same as service worker - always fetch fresh for version checks.


Troubleshooting

Users seeing old version after deployment

Likely causes:

  1. 5-minute HTML cache hasn't expired yet
  2. Browser cache (rare with proper headers)
  3. Service worker cache (should auto-update)

Solutions:

  1. Wait 5 minutes for automatic expiry
  2. Run ./scripts/purge-cache.sh prod for immediate update
  3. User hard refresh: Ctrl+Shift+R / Cmd+Shift+R

Assets returning 404 after deployment

Likely causes:

  1. Build failed to generate assets
  2. Deployment uploaded incomplete dist/
  3. Old HTML referencing new assets (timing issue)

Solutions:

  1. Check GitHub Actions logs for build errors
  2. Verify dist/ contains all assets before deploy
  3. Run ./scripts/purge-cache.sh prod to clear stale HTML

High origin bandwidth usage

Likely causes:

  1. Cache headers misconfigured
  2. Excessive cache purging
  3. CDN not caching properly

Solutions:

  1. Verify headers: curl -I https://ourlantern.app
  2. Check Cloudflare Analytics → Caching
  3. Reduce purge frequency if needed

Best Practices

DO ✅

  • Use cache busting for all versioned assets
  • Keep HTML TTL short (5-10 minutes)
  • Purge selectively (HTML only) on deploy
  • Monitor cache hit rates in Cloudflare
  • Test deployments in dev environment first

DON'T ❌

  • Don't cache HTML for hours (stale references)
  • Don't purge everything on every deploy (wasteful)
  • Don't rely solely on query params for cache busting
  • Don't cache service workers
  • Don't skip version.json generation


Metrics to Monitor

Cloudflare Analytics:

  • Cache hit rate (target: >95% for assets)
  • Bandwidth savings (target: >80% from cache)
  • Origin requests (should be low)

Performance:

  • Time to First Byte (TTFB)
  • Largest Contentful Paint (LCP)
  • Cache invalidation latency

Version checks:

  • Auto-reload frequency
  • Service worker update success rate

Built with VitePress