CDN Optimization with AWS CloudFront + S3: A Real-World Case Study
This post walks through a production rollout of static hosting on Amazon S3 fronted by CloudFront. I cover the architecture, caching strategy, image delivery, CI/CD, security with Origin Access Control (OAC), and the exact results we measured after launch.
Context
At Parma Calcio, we needed a fast, globally distributed way to serve our internal staff portal — especially documents and training videos — with predictable costs, strong cache behavior, and a simple upload + deployment story. Prior to the rollout, content was served from a single region with minimal caching, resulting in high latency for international users and inconsistent cache behavior between pages and assets.
Architecture: S3 + CloudFront + OAC
- S3 (origin): Private bucket for static assets (HTML, CSS, JS, images).
- CloudFront (CDN): Distributes content globally via edge locations.
- OAC: CloudFront Origin Access Control to securely access the bucket.
Key principle: the S3 bucket has no public access. CloudFront accesses S3 using OAC, and end users only hit the CloudFront distribution. This improves security and enables fine-grained cache policies per path.
Cache Policy and Headers
We tuned cache policy by content type. Static assets (hashed files) use long TTLs; HTML gets shorter TTLs to balance freshness and performance. We rely on standard directives:
# HTML (fast iteration, safe staleness)
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60, stale-if-error=86400
# Versioned assets (immutable)
Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable
# Static images (can be versioned too)
Cache-Control: public, max-age=86400, s-maxage=604800, stale-while-revalidate=60, stale-if-error=86400- max-age: Browser cache time.
- s-maxage: CloudFront (shared) cache time.
- stale-while-revalidate: Serve stale while origin fetches a fresh copy.
- stale-if-error: Serve stale if origin is unavailable.
We also ensure correct Content-Type, ETag, and Content-Encoding where relevant. CloudFront handles gzip/br compression at the edge for compressible types.
Invalidation and Versioning Strategy
- File hashing: JS/CSS bundles include a content hash (e.g.
app.3a9c2.js) to enable year-long TTLs without manual invalidations. - HTML: Short TTLs plus targeted CloudFront invalidations for critical content updates (typically
/*only for launches). - Images: Version URL paths when content changes, avoid blanket invalidations.
Image Delivery and Compression
- Formats: Prefer AVIF/WebP; fallback to PNG/JPEG for compatibility when needed.
- Responsive sizing: Upload appropriately sized renditions or use Next.js Image for responsive delivery.
- Compression: CloudFront serves gzip/br for text; images are already compressed at source.
- Documents & videos: Documents (PDF, DOCX) are cached at the edge with appropriate
Cache-Control. Training videos are delivered via CloudFront from S3 with long-lived cache where appropriate; large objects benefit significantly from edge proximity. - Headers: Always set
Cache-Controlon image objects and ensure correctContent-Type.
Uploads: Simple and Safe
For staff uploads we use short-lived pre-signed S3 URLs. This keeps the bucket private, avoids proxying large files through the app server, and ensures consistent object keys and metadata (including Content-Type and Cache-Control) at write time.
Security: OAC and Bucket Policy
Block all public access at the bucket level and only allow CloudFront (via OAC) to read objects. Example bucket policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontAccessOnly",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E2ABCDEFGHIJKL"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
}
]
}With OAC you actually bind CloudFront to S3 at the distribution/origin level and can use a simplified policy. Ensure S3 “Block Public Access” is enabled and only CloudFront can reach the bucket. Always use HTTPS between CloudFront and viewers.
Deliverables
- Edge-cached media center: Centralized delivery of documents and training videos for 50+ staff, with private S3 origin and CloudFront-only access.
- Predictable updates: Versioned asset pipeline and targeted invalidations, preventing cache busting incidents during releases.
- Streamlined uploads: Pre-signed S3 uploads and standardized object metadata for reliable playback and previews.
- Teamwide communication: Faster content distribution and higher availability improved internal communication and data sharing across departments.
Results
Measured Improvements
- 📉 p95 TTFB (EU visitors): 580ms → 65ms
- 🌍 p95 TTFB (US visitors): 820ms → 95ms
- ⚡ p95 LCP: 2.1s → 1.1s
- 🧊 Cache hit ratio: 35% → 92% after versioning rollout
- 💸 Egress costs stabilized with predictable cache behavior
Lessons Learned
- Version aggressively: Long TTLs only work if you can ship new asset URLs; avoid mass invalidations.
- Segment cache policies: HTML vs assets need different TTLs.
- Use OAC: Keep buckets private; let CloudFront be the only door.
- Measure continuously: Watch TTFB, LCP, and cache hit ratio post-deploy.
- Prefer stale-while-revalidate: Great UX during updates and outages.
Additional Resources
- CloudFront + S3 with OAC
- CloudFront cache policies and origin request policies
- CloudFront invalidations
- AWS CLI: s3 sync
- S3 Block Public Access
Planning a CloudFront + S3 rollout? Let's connect.