· NERVICO · cloud-architecture · 10 min read
CDN and CloudFront: Optimal Configuration for Web Performance
Practical Amazon CloudFront guide: step-by-step configuration, cache optimization, compression, WAF security, and real costs for improving web performance.
A user in Madrid accessing a server in Virginia experiences a base latency of 120-150ms. Just from physical distance. Add server processing time, database queries, and asset transfer (images, CSS, JavaScript), and the first page load can take 3-5 seconds.
CloudFront reduces that latency to 10-30ms by serving content from the edge location closest to the user. AWS operates more than 600 points of presence in 100 cities across 50 countries. The difference is noticeable: a page that loads in 1 second instead of 3 not only improves the user experience but directly improves conversion metrics. Google has documented that each additional 100ms of latency reduces sales by 1%.
This article explains how to configure CloudFront optimally, with the parameters that actually matter, the configuration errors that negate CDN benefits, and the real costs you can expect.
How CloudFront Works
The Request Flow
When a user requests a resource through CloudFront, the flow is:
- DNS resolution: The browser resolves the domain. Route 53 (or the configured DNS) responds with the IP of the edge location closest to the user, based on latency or geolocation.
- Edge location: The request reaches the nearest point of presence. CloudFront looks for the resource in its local cache.
- Cache hit: If the resource is in cache and has not expired, CloudFront returns it directly. Minimal latency.
- Cache miss: If the resource is not in cache, CloudFront requests it from the Regional Edge Cache (intermediate cache). If it is not there either, it requests it from the origin (S3, ALB, API Gateway, custom server).
- Response: The resource is returned to the user and stored in cache for future requests.
User (Madrid) -> Edge Location (Madrid) -> Regional Cache (Frankfurt)
| (cache miss)
Origin (S3 / ALB / API)Supported Origins
CloudFront can serve content from multiple origin types:
- Amazon S3: The most common option for static assets (HTML, CSS, JS, images). Native integration with Origin Access Control (OAC).
- Application Load Balancer (ALB): For dynamic content served by applications on ECS, EKS, or EC2.
- API Gateway: For REST or HTTP APIs.
- Lambda@Edge / CloudFront Functions: For edge processing logic (redirects, authentication, personalization).
- Custom origin: Any HTTP/HTTPS server accessible from the internet.
Step-by-Step Configuration
Distribution for a Static Site on S3
The most common case: a static website (React, Next.js, Astro, Vue) deployed on S3 with CloudFront as the CDN.
# Terraform: CloudFront distribution with S3
resource "aws_cloudfront_distribution" "website" {
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
aliases = ["www.yourdomain.com"]
price_class = "PriceClass_100"
http_version = "http2and3"
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_id = "S3Origin"
origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
compress = true
cache_policy_id = aws_cloudfront_cache_policy.optimized.id
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.cors_s3.id
response_headers_policy_id = aws_cloudfront_response_headers_policy.security.id
}
# SPA: redirect 404 to index.html
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
}
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cert.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}
# Origin Access Control (replaces OAI)
resource "aws_cloudfront_origin_access_control" "s3" {
name = "s3-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}Notes on the configuration:
price_class = "PriceClass_100": Uses only edge locations in North America and Europe. Reduces costs 20-30% compared toPriceClass_All. If your users are only in Europe and the Americas, you do not need edge locations in Asia or Oceania.http_version = "http2and3": Enables HTTP/2 and HTTP/3 (QUIC). HTTP/3 reduces initial connection latency by 30-40% because it uses UDP instead of TCP.compress = true: Automatically compresses with Brotli or gzip formats like text/html, application/javascript, text/css, etc. Reduces transfer size by 60-80%.
Cache Policies
CloudFront uses Cache Policies to determine which request parameters affect the cache key. A well-configured policy maximizes the cache hit ratio.
resource "aws_cloudfront_cache_policy" "optimized" {
name = "optimized-cache-policy"
min_ttl = 0
default_ttl = 86400 # 24 hours
max_ttl = 31536000 # 1 year
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none" # Do not include cookies in cache key
}
headers_config {
header_behavior = "none" # Do not include headers in cache key
}
query_strings_config {
query_string_behavior = "none" # Do not include query strings
}
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}The golden rule: The fewer parameters you include in the cache key, the higher the cache hit ratio. Each additional parameter (cookie, header, query string) creates a different cache variant.
For static assets with fingerprinting (e.g., app.a1b2c3.js), the query string does not matter because the hash is in the filename. For APIs, you need to include relevant query strings.
Distribution for Dynamic APIs
For dynamic content (APIs, SSR), the configuration changes:
# Cache behavior for API
ordered_cache_behavior {
path_pattern = "/api/*"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "ALBOrigin"
viewer_protocol_policy = "https-only"
compress = true
# Do not cache by default; respect origin Cache-Control
cache_policy_id = data.aws_cloudfront_cache_policy.caching_disabled.id
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id
}Principle: For APIs, disable CloudFront caching by default and let the origin control behavior with Cache-Control headers. CloudFront respects Cache-Control: max-age=0, no-cache, no-store.
For endpoints returning data that changes infrequently (product catalog, configuration), you can enable caching with a short TTL:
Cache-Control: public, max-age=300, s-maxage=600This means: the browser caches for 5 minutes, CloudFront caches for 10 minutes.
Advanced Optimization
Cache Invalidation vs Versioning
When you update content, you have two options for users to see the new version:
Invalidation: You request CloudFront to remove specific objects from all edge locations.
aws cloudfront create-invalidation \
--distribution-id E1234567890 \
--paths "/index.html" "/css/*" "/js/*"Problems: Invalidations take 5-15 minutes to propagate globally. The first 1,000 paths/month are free; after that, $0.005 per path.
Versioning (recommended): Include a hash or version in the filename. Modern frameworks do this automatically.
app.a1b2c3d4.js (current version)
app.e5f6g7h8.js (new version)With versioning, the new file has a different name. CloudFront requests it from the origin as a new resource. You do not need to invalidate anything. The old file is served from cache until it expires, but nobody requests it because the HTML references the new version.
Exception: index.html cannot have versioning in the filename because it is the entry point. For this file, configure a short TTL (60-300 seconds) or use invalidation after each deployment.
CloudFront Functions vs Lambda@Edge
CloudFront offers two ways to execute code at the edge:
| Feature | CloudFront Functions | Lambda@Edge |
|---|---|---|
| Runtime | JavaScript (limited) | Node.js, Python |
| Memory | 2 MB | 128 MB - 10 GB |
| Max duration | 1 ms | 5s (viewer) / 30s (origin) |
| Events | Viewer request/response | Viewer + origin request/response |
| Cost | $0.10 per million | $0.60 per million + duration |
| Network access | No | Yes |
CloudFront Functions for:
- URL redirects (www to non-www, HTTP to HTTPS)
- Header manipulation (adding security headers)
- URL rewriting (clean URLs, trailing slashes)
- A/B testing based on cookies
// CloudFront Function: add security headers
function handler(event) {
var response = event.response;
var headers = response.headers;
headers['strict-transport-security'] = {
value: 'max-age=63072000; includeSubdomains; preload',
};
headers['x-content-type-options'] = { value: 'nosniff' };
headers['x-frame-options'] = { value: 'DENY' };
headers['x-xss-protection'] = { value: '1; mode=block' };
headers['referrer-policy'] = { value: 'strict-origin-when-cross-origin' };
return response;
}Lambda@Edge for:
- Authentication and authorization
- Dynamic content generation at the edge
- A/B testing with complex logic
- Personalization by geolocation
Compression: Brotli vs gzip
CloudFront supports automatic compression with Brotli and gzip. Brotli offers 15-25% better compression ratio than gzip for web content, but only works over HTTPS (which should be 100% of your traffic).
| File Type | Uncompressed | gzip | Brotli | Brotli vs gzip Savings |
|---|---|---|---|---|
| HTML (100 KB) | 100 KB | 25 KB | 20 KB | 20% smaller |
| JavaScript (500 KB) | 500 KB | 120 KB | 95 KB | 21% smaller |
| CSS (80 KB) | 80 KB | 18 KB | 14 KB | 22% smaller |
| JSON (200 KB) | 200 KB | 45 KB | 36 KB | 20% smaller |
CloudFront automatically selects the best algorithm based on the browser’s Accept-Encoding header. You do not need to configure anything beyond compress = true.
Security with CloudFront
AWS WAF
CloudFront integrates natively with AWS WAF (Web Application Firewall) to protect against common attacks:
resource "aws_wafv2_web_acl" "cloudfront" {
name = "cloudfront-waf"
scope = "CLOUDFRONT"
default_action {
allow {}
}
# SQL injection protection
rule {
name = "aws-sql-injection"
priority = 1
override_action { none {} }
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "sql-injection"
}
}
# Rate limiting: 2000 requests per IP in 5 minutes
rule {
name = "rate-limit"
priority = 2
action { block {} }
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "rate-limit"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "cloudfront-waf"
}
}WAF cost: $5/month per Web ACL + $1/month per rule + $0.60 per million requests evaluated. For a site with 10 million requests per month and 5 rules, the cost is approximately $16/month.
Origin Access Control (OAC)
If your origin is S3, OAC ensures users can only access content through CloudFront. The S3 bucket is not directly accessible by URL.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-web-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789:distribution/E1234567890"
}
}
}
]
}HTTPS and Certificates
CloudFront requires SSL/TLS certificates from AWS Certificate Manager (ACM). ACM certificates are free and renew automatically. The only requirement is that the certificate must be in the us-east-1 region (a global CloudFront requirement).
Real CloudFront Costs
Billing Model
CloudFront charges for three items:
- Data transfer to users: From $0.085/GB (first 10 TB/month) down to $0.020/GB (over 5 PB/month).
- HTTP/HTTPS requests: $0.0075 per 10,000 HTTPS requests (North America/Europe).
- Invalidations: First 1,000 paths/month free; then $0.005 per path.
Cost Scenarios
| Scenario | Traffic | Requests | Monthly Cost |
|---|---|---|---|
| Blog/landing page | 50 GB | 2M | $5-8 |
| Medium SaaS application | 500 GB | 20M | $50-70 |
| E-commerce with heavy assets | 5 TB | 100M | $450-550 |
| Streaming platform | 50 TB | 500M | $3,500-4,500 |
Savings vs Serving from Origin
CloudFront not only improves latency; it can also reduce costs. Data transfer directly from S3 costs $0.09/GB. From CloudFront, the first 10 TB cost $0.085/GB, but 1 TB free per month is included in the Free Tier. Additionally, CloudFront requests to the origin (S3) are free.
For sites with predominantly cacheable traffic, CloudFront can be cheaper than serving directly from S3 or ALB.
Monitoring and Metrics
Key CloudWatch Metrics
- Cache hit ratio: The percentage of requests served from cache. Target: above 90% for static assets.
- Error rate: Percentage of 4xx and 5xx responses. Target: under 1%.
- Latency: Time to first byte (TTFB). Target: under 50ms for cached assets.
- Bytes transferred: Total volume of data served. For cost estimation.
# Query cache hit ratio for the last 24 hours
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=E1234567890 \
--start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics AverageCloudFront Access Logs
Enable access logs to analyze traffic patterns, identify uncached content, and detect abuse.
Logs are stored in S3 and can be analyzed with Athena:
SELECT
date,
time,
sc_status,
cs_uri_stem,
x_edge_result_type,
time_taken
FROM cloudfront_logs
WHERE x_edge_result_type = 'Miss'
AND date = '2025-09-24'
ORDER BY time_taken DESC
LIMIT 20;Common Configuration Mistakes
Mistake 1: Caching Personalized Content
If your page includes the user’s name in the HTML and you cache it without including the session cookie in the cache key, one user will see another user’s name. For personalized content, use Cache-Control: private, no-store or include an identifier in the cache key.
Mistake 2: TTL Too Long on index.html
If you set a 24-hour TTL for index.html and deploy an update, users will see the old version for up to 24 hours. Set a TTL of 60-300 seconds for entry files and use post-deployment invalidation.
Mistake 3: Not Enabling Compression
CloudFront can automatically compress content, but you need to enable compress = true. Without compression, you are transferring 3-5 times more data than necessary, paying more in transfer, and delivering a worse experience.
Mistake 4: Ignoring HTTP/3
HTTP/3 uses QUIC over UDP, which eliminates the head-of-line blocking of HTTP/2 and reduces connection establishment time. CloudFront supports HTTP/3 at no additional cost. Enabling it is a free performance improvement.
Conclusion
CloudFront is not an optional add-on. For any web application serving users in multiple geographic locations, a correctly configured CDN is the most impactful and cheapest performance improvement you can implement.
The optimal configuration is not complicated, but it requires attention to detail: restrictive cache policies to maximize the hit ratio, compression enabled, HTTP/3 active, security headers at the edge, and appropriate TTLs for each content type.
If you need help optimizing your application’s content distribution or configuring CloudFront for a complex use case, our AWS consulting team has direct experience with these configurations. Request a free audit to evaluate the current performance of your web infrastructure.