CDN/Edge Caching Implementation Guide¶
Date: 2025-10-25 Project: PiqueTickets Frontend Goal: Move caching from Railway origin server to edge/CDN for 400-500MB memory reduction
Table of Contents¶
- Overview
- Option 1: Railway + Cloudflare CDN (Recommended)
- Option 2: Migrate to Vercel (Alternative)
- Option 3: Railway + AWS CloudFront
- Performance Testing
- Rollback Plan
Overview¶
Current Architecture¶
Target Architecture (Railway + Cloudflare)¶
User → Cloudflare CDN → Railway (Next.js) → Django API
↑ ↑
Caching here Minimal caching
(edge servers) (50-100MB memory)
Benefits¶
- 400-500MB memory reduction on Railway origin
- Faster page loads (edge servers closer to users)
- Lower Railway costs (smaller container needed)
- Better DDoS protection (Cloudflare handles traffic spikes)
- Global distribution (edge caching worldwide)
Option 1: Railway + Cloudflare CDN (Recommended)¶
Cost: Free (Cloudflare Free tier is sufficient) Time: 1-2 hours Complexity: Low Memory Reduction: 400-500MB
Step 1: Sign Up for Cloudflare¶
- Go to cloudflare.com
- Create free account
- Click "Add a Site"
- Enter
piquetickets.com - Select Free Plan
Step 2: Update Domain Nameservers¶
Cloudflare will provide nameservers like:
Update at your domain registrar (GoDaddy, Namecheap, etc.):
Example for Namecheap: 1. Log in to Namecheap 2. Go to Domain List → Manage 3. Navigate to Nameservers 4. Select "Custom DNS" 5. Enter Cloudflare nameservers 6. Save (propagation takes 0-24 hours)
Verify DNS propagation:
Step 3: Configure DNS Records in Cloudflare¶
In Cloudflare Dashboard → DNS → Records:
Type Name Value Proxy Status
----------------------------------------------------------------------
A piquetickets.com your-railway-ip-address Proxied (orange cloud)
CNAME www piquetickets.com Proxied (orange cloud)
A api your-api-railway-ip Proxied (orange cloud)
CNAME api-staging staging-railway.up.railway.app Proxied (orange cloud)
Get Railway IP Address:
# In Railway dashboard for your frontend service
# Settings → Networking → Public Networking → Copy the domain
# Then resolve it:
nslookup piquetickets.up.railway.app
# Or use Railway's custom domain directly if already set up
Important: Make sure "Proxy status" is Proxied (orange cloud icon) for CDN caching to work.
Step 4: Configure Cloudflare SSL/TLS¶
Cloudflare Dashboard → SSL/TLS:
- SSL/TLS encryption mode: Full (strict)
-
Encrypts traffic between user ↔ Cloudflare ↔ Railway
-
Edge Certificates: Automatic (already enabled)
-
Always Use HTTPS: ON
- Settings → SSL/TLS → Edge Certificates → Always Use HTTPS: ON
Step 5: Update Next.js Configuration for CDN¶
File: apps/frontend/next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// ... existing config
// IMPORTANT: Remove or reduce page-level caching
// We'll rely on CDN/edge caching instead
experimental: {
optimizeCss: true,
optimizePackageImports: ['react-icons'],
webpackMemoryOptimizations: true,
preloadEntriesOnStart: false,
// Reduce origin cache size dramatically
isrMemoryCacheSize: 10 * 1024 * 1024, // 10MB (down from 50MB)
isrFlushToDisk: true,
},
// ... existing image config
// Add CDN-friendly headers
async headers() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'https://api.piquetickets.com';
const apiDomain = new URL(apiUrl).origin;
return [
// Security headers for all routes (existing)
{
source: '/(.*)',
headers: [
// ... your existing security headers
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
// ... rest of existing headers
]
},
// CDN caching for homepage
{
source: '/',
headers: [
{
key: 'Cache-Control',
// Browser: 0s, CDN: 5min, stale-while-revalidate: 1hr
value: 'public, s-maxage=300, stale-while-revalidate=3600',
},
{
key: 'CDN-Cache-Control',
// Cloudflare-specific: cache for 1 hour
value: 'public, max-age=3600',
},
],
},
// CDN caching for show pages
{
source: '/shows/:path*',
headers: [
{
key: 'Cache-Control',
// Browser: 0s, CDN: 10min, stale: 1hr
value: 'public, s-maxage=600, stale-while-revalidate=3600',
},
{
key: 'CDN-Cache-Control',
value: 'public, max-age=3600',
},
],
},
// CDN caching for producer pages
{
source: '/producers/:path*',
headers: [
{
key: 'Cache-Control',
// Producers change less frequently
value: 'public, s-maxage=1800, stale-while-revalidate=7200',
},
{
key: 'CDN-Cache-Control',
value: 'public, max-age=7200', // 2 hours
},
],
},
// CDN caching for venue pages
{
source: '/venue/:path*',
headers: [
{
key: 'Cache-Control',
// Venues change rarely
value: 'public, s-maxage=3600, stale-while-revalidate=86400',
},
{
key: 'CDN-Cache-Control',
value: 'public, max-age=86400', // 24 hours
},
],
},
// CDN caching for static pages
{
source: '/(about|privacy-policy|terms-of-service|refund-policies)/:path*',
headers: [
{
key: 'Cache-Control',
// Static content - cache aggressively
value: 'public, s-maxage=86400, stale-while-revalidate=604800',
},
{
key: 'CDN-Cache-Control',
value: 'public, max-age=604800', // 1 week
},
],
},
// NO caching for API routes
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'private, no-cache, no-store, must-revalidate',
},
],
},
];
},
// ... rest of config
};
export default nextConfig;
Step 6: Update Page-Level Revalidation¶
Remove or reduce page-level revalidate exports:
File: apps/frontend/src/app/page.tsx
// BEFORE:
export const revalidate = 30;
// AFTER:
// Remove this line entirely - rely on CDN caching
// OR keep very short for fallback:
export const revalidate = 300; // 5 minutes (only for origin cache)
File: apps/frontend/src/app/shows/[slug]/page.tsx
// BEFORE:
export const revalidate = 30;
// AFTER:
export const revalidate = 600; // 10 minutes origin cache
// CDN will cache for 1 hour (per headers above)
Apply same pattern to:
- apps/frontend/src/app/shows/page.tsx
- apps/frontend/src/app/producers/page.tsx
- apps/frontend/src/app/producers/[slug]/page.tsx
- apps/frontend/src/app/venue/[id]/page.tsx
- apps/frontend/src/app/series/[slug]/page.tsx
Step 7: Configure Cloudflare Page Rules¶
Cloudflare Dashboard → Rules → Page Rules:
Rule 1: Cache Everything for HTML
URL Pattern: piquetickets.com/*
Settings:
- Cache Level: Standard
- Edge Cache TTL: 1 hour
- Browser Cache TTL: Respect Existing Headers
- Origin Cache Control: ON
Rule 2: Bypass Cache for API
Rule 3: Cache Static Assets Aggressively
URL Pattern: piquetickets.com/_next/static/*
Settings:
- Cache Level: Cache Everything
- Edge Cache TTL: 1 year
- Browser Cache TTL: 1 year
Note: Free plan allows 3 page rules. If you need more, upgrade to Pro ($20/month).
Step 8: Configure Cloudflare Cache Rules (New Method)¶
Alternative to Page Rules (better, more rules on free tier):
Cloudflare Dashboard → Caching → Cache Rules → Create Rule
Rule 1: Cache HTML Pages
Rule name: Cache HTML Pages
When incoming requests match:
(http.request.uri.path matches "^/shows/.*") or
(http.request.uri.path matches "^/producers/.*") or
(http.request.uri.path matches "^/venue/.*") or
(http.request.uri.path eq "/")
Then:
Cache eligibility: Eligible for cache
Edge TTL: 1 hour
Browser TTL: 5 minutes
Respect Origin TTL: Yes
Rule 2: Bypass API Routes
Rule name: Bypass API Cache
When incoming requests match:
http.request.uri.path matches "^/api/.*"
Then:
Cache eligibility: Bypass cache
Rule 3: Cache Static Assets
Rule name: Cache Static Assets
When incoming requests match:
http.request.uri.path matches "^/_next/static/.*" or
http.request.uri.path matches "^/static/.*" or
http.request.uri.path matches ".*\\.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|webp|avif)$"
Then:
Cache eligibility: Eligible for cache
Edge TTL: 1 year
Browser TTL: 1 year
Step 9: Test CDN Caching¶
Test cache headers:
# Test homepage
curl -I https://piquetickets.com
# Look for:
# cf-cache-status: HIT (cached)
# cf-cache-status: MISS (not cached, first request)
# cf-cache-status: DYNAMIC (bypassed)
# cf-ray: [ray-id] (confirms going through Cloudflare)
# Test show page
curl -I https://piquetickets.com/shows/some-show-slug
# Test API (should be DYNAMIC/bypassed)
curl -I https://piquetickets.com/api/cache/health
Test cache hit rates:
# Make request twice
curl -I https://piquetickets.com/shows/some-show
# First: cf-cache-status: MISS
curl -I https://piquetickets.com/shows/some-show
# Second: cf-cache-status: HIT ✓
Step 10: Purge Cache When Needed¶
Option A: Cloudflare Dashboard 1. Cloudflare Dashboard → Caching → Configuration 2. Click "Purge Everything" or "Purge by URL"
Option B: Cloudflare API (Recommended)
Create cache purge script:
File: apps/frontend/scripts/purge-cdn-cache.sh
#!/bin/bash
# Cloudflare credentials (add to Railway env vars)
CLOUDFLARE_ZONE_ID="${CLOUDFLARE_ZONE_ID}"
CLOUDFLARE_API_KEY="${CLOUDFLARE_API_KEY}"
# Purge specific URLs
curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CLOUDFLARE_API_KEY}" \
-H "Content-Type: application/json" \
--data '{
"files": [
"https://piquetickets.com/",
"https://piquetickets.com/shows"
]
}'
# Or purge everything
# --data '{"purge_everything": true}'
# Or purge by tag (requires Enterprise plan)
# --data '{"tags": ["shows"]}'
Option C: Integrate with Next.js Cache Purge API
Update your cache purge API to also purge Cloudflare:
File: apps/frontend/src/app/api/cache/purge/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
async function purgeCloudflareCache(urls?: string[]) {
const zoneId = process.env.CLOUDFLARE_ZONE_ID
const apiKey = process.env.CLOUDFLARE_API_KEY
if (!zoneId || !apiKey) {
console.warn('Cloudflare credentials not configured')
return
}
try {
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(
urls
? { files: urls }
: { purge_everything: true }
),
}
)
const data = await response.json()
console.log('Cloudflare purge result:', data)
} catch (error) {
console.error('Cloudflare purge failed:', error)
}
}
export async function POST(request: NextRequest) {
// Verify authorization
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.CACHE_PURGE_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { tags, paths, purgeCloudflare = false } = await request.json()
try {
// Purge Next.js cache
if (tags && Array.isArray(tags)) {
tags.forEach(tag => revalidateTag(tag))
}
if (paths && Array.isArray(paths)) {
paths.forEach(path => revalidatePath(path))
}
// Optionally purge Cloudflare CDN
if (purgeCloudflare) {
const urls = paths?.map((path: string) =>
`https://piquetickets.com${path}`
)
await purgeCloudflareCache(urls)
}
return NextResponse.json({
success: true,
purged: { tags, paths, cloudflare: purgeCloudflare },
timestamp: new Date().toISOString()
})
} catch (error) {
return NextResponse.json({
error: 'Cache purge failed',
details: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 })
}
}
Add environment variables to Railway:
Get Cloudflare credentials: 1. Zone ID: Cloudflare Dashboard → Overview (right sidebar) 2. API Key: Cloudflare Dashboard → My Profile → API Tokens → Create Token - Use "Edit zone DNS" template - Zone Resources: Include → Specific zone → piquetickets.com - Copy the token
Step 11: Monitor CDN Performance¶
Cloudflare Analytics: - Dashboard → Analytics & Logs - View: - Requests (total traffic) - Bandwidth (saved from origin) - Cache hit rate (should be >80%) - Response time
Check memory reduction on Railway:
# Before CDN: ~400-600MB
# After CDN: ~100-200MB (target)
# Monitor via Railway dashboard:
# Service → Metrics → Memory Usage
Step 12: Configure Cache for Images¶
Cloudflare automatically caches images, but optimize further:
File: apps/frontend/next.config.ts
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 3600, // 1 hour (CDN caches longer)
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Let Cloudflare handle image optimization
unoptimized: false, // Keep Next.js optimization for first pass
},
Cloudflare Image Optimization (Optional, $5/month): - Dashboard → Speed → Optimization - Enable: - Polish: Lossy - Mirage: ON (lazy loading) - WebP: ON
Option 2: Migrate to Vercel (Alternative)¶
Cost: Free tier available, scales with usage Time: 2-4 hours Complexity: Medium (requires migration) Memory Reduction: 100% (serverless, no persistent memory)
Why Vercel?¶
- Built-in edge caching (global CDN included)
- Serverless Next.js (no memory accumulation issues)
- Zero config for Next.js apps
- Built by Next.js creators (best compatibility)
- Edge runtime available for ultra-fast responses
Downsides¶
- More expensive at scale ($20/month Pro plan for team features)
- Vendor lock-in
- Need to migrate from Railway
Migration Steps¶
Step 1: Create Vercel Account¶
- Go to vercel.com
- Sign up with GitHub
- Install Vercel CLI:
Step 2: Connect Repository¶
# In your project directory
cd apps/frontend
# Login to Vercel
vercel login
# Deploy to Vercel
vercel
# Follow prompts:
# - Set up and deploy? Yes
# - Which scope? Your account/team
# - Link to existing project? No
# - Project name? piquetickets-frontend
# - Directory? ./
# - Override settings? No
Step 3: Configure Environment Variables¶
Vercel Dashboard → Project → Settings → Environment Variables
Add all your env vars:
NEXT_PUBLIC_API_URL=https://api.piquetickets.com
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=...
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=...
NEXT_PUBLIC_CDN_DOMAIN=...
CACHE_PURGE_SECRET=...
Step 4: Configure Custom Domain¶
Vercel Dashboard → Project → Settings → Domains
- Add domain:
piquetickets.com - Vercel will provide DNS records:
- Update at your domain registrar
- Wait for DNS propagation (0-24 hours)
Step 5: Update Next.js Config for Vercel Edge¶
File: apps/frontend/src/app/shows/[slug]/page.tsx
// Add edge runtime for fastest responses
export const runtime = 'edge' // Run on Vercel Edge Network
// Keep revalidation
export const revalidate = 60
Apply to high-traffic pages:
- Homepage (app/page.tsx)
- Show pages (app/shows/[slug]/page.tsx)
- Shows listing (app/shows/page.tsx)
Step 6: Configure Vercel Edge Config (Optional)¶
For advanced caching control:
File: apps/frontend/vercel.json
{
"headers": [
{
"source": "/shows/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, s-maxage=600, stale-while-revalidate=3600"
}
]
},
{
"source": "/_next/static/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
],
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.piquetickets.com/:path*"
}
]
}
Step 7: Deploy Production¶
# Deploy to production
vercel --prod
# Or connect GitHub for auto-deployments:
# Vercel Dashboard → Git → Connect Git Repository
# Every push to main = automatic deployment
Step 8: Monitor Performance¶
Vercel Dashboard → Analytics: - View edge cache hit rate - Response times by region - Traffic breakdown - Core Web Vitals
Option 3: Railway + AWS CloudFront¶
Cost: Pay-as-you-go (typically $10-50/month) Time: 4-6 hours Complexity: High Memory Reduction: 400-500MB
Brief Overview¶
- Create AWS CloudFront distribution
- Set origin to Railway URL
- Configure cache behaviors
- Update DNS to point to CloudFront
- Configure SSL certificate
Not recommended unless you: - Already use AWS - Need advanced features Cloudflare doesn't offer - Have DevOps expertise
For most cases, Cloudflare is easier and free.
Performance Testing¶
Before CDN (Baseline)¶
# Test from multiple locations
# Use https://www.webpagetest.org/
# Test from command line
time curl -w "@curl-format.txt" -o /dev/null -s https://piquetickets.com
# Create curl-format.txt:
cat > curl-format.txt <<EOF
time_namelookup: %{time_namelookup}s\n
time_connect: %{time_connect}s\n
time_appconnect: %{time_appconnect}s\n
time_pretransfer: %{time_pretransfer}s\n
time_redirect: %{time_redirect}s\n
time_starttransfer: %{time_starttransfer}s\n
----------\n
time_total: %{time_total}s\n
EOF
After CDN¶
# Same test - should be 2-5x faster
time curl -w "@curl-format.txt" -o /dev/null -s https://piquetickets.com
# Check cache status
curl -I https://piquetickets.com | grep -i cache
Load Testing¶
# Use Apache Bench
ab -n 1000 -c 50 https://piquetickets.com/
# Before CDN: ~500-800ms avg
# After CDN: ~100-200ms avg (goal)
Rollback Plan¶
If CDN Causes Issues¶
Cloudflare: 1. Cloudflare Dashboard → DNS 2. Toggle all records from "Proxied" (orange) to "DNS only" (gray) 3. Traffic bypasses Cloudflare, goes direct to Railway 4. Diagnose issue 5. Re-enable proxy when fixed
Vercel: 1. Update DNS back to Railway 2. Delete Vercel deployment 3. Re-deploy on Railway
Common Issues¶
Issue: Stale content showing
# Purge Cloudflare cache
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
Issue: API requests failing
- Check bypass rule for /api/*
- Verify CORS headers
- Check Cloudflare firewall rules
Issue: SSL certificate errors - Cloudflare: Ensure SSL mode is "Full (strict)" - Verify Railway has valid SSL cert
Success Criteria¶
- CDN cache hit rate > 80%
- Railway memory usage < 150MB
- Page load time (p95) < 500ms
- No increase in error rates
- Successful cache purges
- Cost within budget ($0-20/month for Cloudflare)
Cost Comparison¶
| Solution | Monthly Cost | Setup Time | Memory Reduction | Best For |
|---|---|---|---|---|
| Cloudflare Free | $0 | 1-2 hours | 400-500MB | Most projects |
| Cloudflare Pro | $20 | 1-2 hours | 400-500MB | More page rules needed |
| Vercel Hobby | $0 | 2-4 hours | 100% (serverless) | Small projects |
| Vercel Pro | $20 | 2-4 hours | 100% (serverless) | Team features needed |
| AWS CloudFront | $10-50 | 4-6 hours | 400-500MB | Already on AWS |
Recommendation: Start with Cloudflare Free + Railway (no cost increase, best bang for buck)
Last Updated: 2025-10-25 Next Steps: Implement Option 1 (Cloudflare) if approved