Separating Real-Time & Cacheable Content: A Multi-Tier CloudFront Strategy
At scale, CDN cache invalidation becomes a coordination nightmare. Every deploy risks serving stale HTML, CSS, or JavaScript to active customers. Traditional approaches—blanket invalidations on every deploy, shorter TTLs, or no caching at all—trade reliability for performance. We built a different solution: separate CloudFront distributions for content that needs instant updates versus content that can breathe.
The Problem: Stale Cache at Scale
Web properties serving customers in real time (proposal generation, crew rosters, booking confirmations) cannot afford a 5-second or 5-minute cache window. During peak booking periods, a stale charter proposal means a lost transaction. Meanwhile, marketing sites and static landing pages can safely live behind aggressive caching—if we can guarantee the cache is cleared only when content actually changes.
The risk: invalidate too much, and you're burning CloudFront API calls and serving cache-miss traffic spikes; invalidate too little, and you're liable to serve outdated copy to prospects.
The Architecture: Three-Tier Distribution Pattern
We separated concerns into three CloudFront distributions:
- No-Cache Distribution (EPF415U2AO8B3): Serves
/print/*and/g/*endpoints withCachingDisabled. PDFs, manifests, crew rosters, and on-demand dynamic content bypass CloudFront caching entirely. S3 uploads to these paths go live instantly with zero invalidation overhead. - Web Distribution (main production): Serves marketing copy, landing pages, and HTML shells with standard cache headers (TTL varies by content type). Every deploy auto-invalidates
/*—a single atomic operation bundled into the deploy tool. - Staging Distribution (EJJKHNHYNK4Q5): Isolated sandbox for previewing changes before production. Same caching rules as production, zero prod impact during testing.
Each distribution has its own S3 origin bucket (or prefix) and explicit cache behaviors. The separation means a stale proposal never interferes with a stale homepage, and vice versa.
Technical Implementation
CloudFront Configuration:
Production (EPF415U2AO8B3):
Cache Behavior 1: /print/* and /g/*
→ Caching Disabled (CachingDisabled=true)
→ No TTL, no validation
Cache Behavior 2: /* (default)
→ TTL: 3600 (HTML), 86400 (CSS/JS)
→ Auto-invalidate on deploy
Staging (EJJKHNHYNK4Q5):
Same structure, gated to staging.qos domain
The Deploy Tool:
All deployment logic funnels through a single tool: ~/bin/jada-deploy. This tool:
- Uploads changed files to S3 (respecting path-based rules)
- Automatically invalidates the production distribution (if not a no-cache path)
- Skips invalidation for
/print/*and/g/*(safe, already CachingDisabled) - Optionally waits for invalidation to complete (
--wait) or batches multiple deployments (--all)
# Deploy a web change (auto-invalidates /* on EPF415U2AO8B3)
~/bin/jada-deploy --web
# Deploy a print endpoint (no invalidation needed, instant live)
~/bin/jada-deploy --print
# Deploy everything and wait for edge distribution
~/bin/jada-deploy --all --wait
S3 Bucket Structure:
s3://dist-jada/
/print/ ← served by /print/* behavior
/g/ ← served by /g/* behavior
/index.html ← served by /* behavior, cached 3600s
/assets/ ← served by /* behavior, cached 86400s
...
Key Decisions
Why three distributions? A single distribution with complex cache rules becomes a debugging nightmare at 3 AM. Three distributions, each with one clear job, means you can reason about each independently. If crew rosters lag, you know to check EPF415U2AO8B3's S3 origin, not invalidation logic.
Why disable caching for /print/* and /g/? These endpoints generate PDFs and dynamic rosters on demand. Content changes on every request (timestamps, unique IDs, personalization). Caching would require sophisticated cache-busting keys—simpler to bypass caching and let S3 + origin response time (usually <100ms) be the latency story.
Why auto-invalidate on every web deploy? Web content (HTML, marketing copy) changes less frequently than print output but more frequently than we can reason about cache TTLs. One deploy tool, one invalidation behavior, eliminates the coordination problem. The cost (CloudFront API calls, edge distribution latency) is acceptable because web deploys are infrequent (<5/week).
Staging distribution? Isolates preview environments from production. Stagingredirects to .staging.qos domain, same cache rules, zero prod risk.
Monitoring & Observability
Logs are kept in ~/icloud-jada-ops/ for every deploy. CloudFront metrics are available via AWS Console (cache hit ratio, origin latency, invalidation status). A simple health check (not yet instrumented but planned) will monitor /print/* and /g/* response times to catch origin slowness.
What's Next
This pattern has eliminated stale-cache incidents in production. Future improvements:
- Fine-grained invalidation: Instead of invalidating
/*, invalidate only changed file paths. Requires tracking file diffs in deploy tool. - Cache tags: CloudFront's newer
Cache-Keyheaders allow grouping invalidations by semantic tag (e.g., all crew rosters) rather than path glob. - Origin shield: Add a regional origin shield to reduce S3 origin load during cache misses.
- Staged rollouts: Deploy to staging, monitor metrics, then promote to production without invalidation (requires S3 atomic swap or blue-green origin).
The architecture scales with the number of properties—each queen-of-city staging site (queenof-cities factory at ~/icloud-repos/sites/queenof-cities/, dist E176JRMB92GPAJ) and side projects (Dragon Bodyguards E10WJ716X823DQ) inherit the same pattern. One tool, three tiers, deterministic behavior.