How to Set Cache-Control Headers in CloudFront (Without Functions)
A comprehensive guide to configuring Cache-Control headers in AWS CloudFront using Response Header Policies, including best practices and optimisation strategies.
How to Set Cache-Control Headers in CloudFront (Without Functions)
Proper cache configuration is critical for web performance, SEO, and cost optimisation. CloudFront’s Response Header Policies provide a straightforward way to add or modify Cache-Control headers without writing CloudFront Functions or Lambda@Edge code.
In this guide, I’ll show you how to configure Cache-Control headers in CloudFront, share best practices from production environments, and explain common pitfalls to avoid.
What is Cache-Control?
The Cache-Control HTTP header controls how browsers and CDNs cache your content. It determines:
- How long content can be cached (
max-age) - Where it can be cached (public vs. private)
- When it must be revalidated (
no-cache,must-revalidate) - If it can be cached at all (
no-store)
Common Cache-Control Directives
Cache-Control: public, max-age=31536000, immutable
Cache-Control: public, max-age=3600, must-revalidate
Cache-Control: private, no-cache
Cache-Control: no-store
Key Directives:
public- Can be cached by browsers and CDNsprivate- Can only be cached by browsers, not CDNsmax-age=<seconds>- How long to cache (in seconds)s-maxage=<seconds>- CDN-specific cache duration (overrides max-age for shared caches)immutable- Content will never change (perfect for versioned assets)no-cache- Must revalidate with origin before using cached copyno-store- Don’t cache at all (sensitive data)must-revalidate- Must check with origin if cache expires
Why Use Response Header Policies?
Before Response Header Policies, you had three options for adding headers in CloudFront:
- Origin configuration - Requires backend changes
- CloudFront Functions - JavaScript code executed at edge
- Lambda@Edge - More powerful but more expensive
Response Header Policies are better because:
- No code to write or maintain
- No execution costs
- Immediate configuration changes
- Can override origin headers
- Reusable across distributions
Step-by-Step Configuration
Method 1: Using AWS Console (Recommended for Beginners)
1. Create a Response Header Policy
Navigate to CloudFront in the AWS Console:
CloudFront → Policies → Response headers → Create response headers policy
Configuration:
Policy Name: cache-control-static-assets
Description: Long-term caching for versioned static assets
Security headers: (optional)
Custom headers:
- Header name: Cache-Control
- Header value: public, max-age=31536000, immutable
- Override origin: Yes (if you want CloudFront to override origin headers)
When to override origin:
- ✅ Yes - When you want consistent caching regardless of origin configuration
- ❌ No - When origin should control caching (dynamic content, A/B testing)
2. Attach Policy to Behavior
Navigate to your CloudFront distribution:
CloudFront → Distributions → [Your Distribution] → Behaviors tab
Steps:
- Select the behavior you want to modify (usually
Default (*)) - Click “Edit”
- Scroll to Response headers policy
- Select your newly created policy
- Click “Save changes”
Wait 5-10 minutes for the distribution to deploy changes across all edge locations.
3. Verify Configuration
Test the headers with curl:
curl -I https://your-distribution.cloudfront.net/asset.js
HTTP/2 200
cache-control: public, max-age=31536000, immutable
content-type: application/javascript
age: 42
x-cache: Hit from cloudfront
What to look for:
- ✅
cache-controlheader matches your policy - ✅
x-cache: Hit from cloudfront(indicates caching is working) - ✅
ageheader showing how long content has been cached
Method 2: Using AWS CLI (Recommended for IaC)
Create the Response Header Policy
aws cloudfront create-response-headers-policy \
--response-headers-policy-config '{
"Name": "cache-control-static-assets",
"Comment": "Long-term caching for versioned assets",
"CustomHeadersConfig": {
"Quantity": 1,
"Items": [
{
"Header": "Cache-Control",
"Value": "public, max-age=31536000, immutable",
"Override": true
}
]
}
}'
Update Distribution Behavior
# Get current distribution config
aws cloudfront get-distribution-config \
--id E1234EXAMPLE \
--output json > distribution-config.json
# Edit distribution-config.json and add ResponseHeadersPolicyId to DefaultCacheBehavior
# Then update:
aws cloudfront update-distribution \
--id E1234EXAMPLE \
--distribution-config file://distribution-config.json \
--if-match <ETag-from-get-distribution-config>
Method 3: Using Terraform
# Create Response Header Policy
resource "aws_cloudfront_response_headers_policy" "cache_control_static" {
name = "cache-control-static-assets"
comment = "Long-term caching for versioned static assets"
custom_headers_config {
items {
header = "Cache-Control"
override = true
value = "public, max-age=31536000, immutable"
}
}
}
# Attach to CloudFront Distribution
resource "aws_cloudfront_distribution" "main" {
# ... other configuration ...
default_cache_behavior {
# ... other settings ...
response_headers_policy_id = aws_cloudfront_response_headers_policy.cache_control_static.id
}
ordered_cache_behavior {
path_pattern = "/api/*"
# Different policy for API endpoints
response_headers_policy_id = aws_cloudfront_response_headers_policy.cache_control_api.id
# ... other settings ...
}
}
Best Practices by Content Type
Static Assets (Versioned)
Content: /assets/app-v1.2.3.js, /dist/bundle.[hash].css
Cache-Control: public, max-age=31536000, immutable
Why:
- Files include version/hash in name
- Will never change
immutableprevents unnecessary revalidationmax-age=31536000= 1 year (maximum recommended)
HTML Pages
Content: /index.html, /about.html
Cache-Control: public, max-age=3600, must-revalidate
Why:
- Content changes frequently
- 1 hour cache (
3600seconds) must-revalidateensures fresh content after expiry- Short TTL balances performance and freshness
API Responses (Cacheable)
Content: /api/products, /api/public-data
Cache-Control: public, max-age=300, s-maxage=900
Why:
- Browser caches for 5 minutes (
max-age=300) - CloudFront caches for 15 minutes (
s-maxage=900) - Longer CDN cache reduces origin load
- Short browser cache keeps data reasonably fresh
API Responses (Non-Cacheable)
Content: /api/user/profile, /api/cart
Cache-Control: private, no-cache, no-store, must-revalidate
Why:
- User-specific data
privateprevents CDN cachingno-storeprevents any caching- Critical for security
Images (Static)
Content: /images/logo.png, /photos/product-123.jpg
Cache-Control: public, max-age=2592000
Why:
- 30 days cache (
2592000seconds) - Images change infrequently
- Balance between performance and updates
Multiple Cache Behaviors for Different Paths
You can create multiple Response Header Policies and apply them to different path patterns:
Behavior Path Pattern Cache-Control Header
-----------------------------------------------------------------
Static Assets /assets/* public, max-age=31536000, immutable
HTML Pages *.html public, max-age=3600
API Endpoints /api/* private, no-cache
Images /images/* public, max-age=2592000
Default * public, max-age=86400
Configure in CloudFront:
- Create separate Response Header Policies for each pattern
- Add ordered cache behaviors in your distribution
- Match path patterns to specific policies
Common Issues and Troubleshooting
Issue: Headers Not Appearing
Check:
- Distribution status is “Deployed”
- Wait 5-10 minutes after configuration change
- Clear browser cache (
Ctrl+Shift+RorCmd+Shift+R) - Test with
curl -Ito bypass browser cache - Check CloudFront access logs
Solution:
# Invalidate CloudFront cache to force refresh
aws cloudfront create-invalidation \
--distribution-id E1234EXAMPLE \
--paths "/*"
Issue: Origin Headers Override CloudFront
Problem: Origin sends Cache-Control: no-cache which overrides your policy.
Solution: Enable “Override origin” in your Response Header Policy:
custom_headers_config {
items {
header = "Cache-Control"
override = true # ← Force this value
value = "public, max-age=31536000"
}
}
Issue: Low Cache Hit Ratio
Causes:
- Query strings causing cache key variations
- Cookies included in cache key
- Headers forwarded to origin
Solution: Configure cache key and origin request policies:
cache_policy_id = aws_cloudfront_cache_policy.optimised.id
# Create optimised cache policy
resource "aws_cloudfront_cache_policy" "optimised" {
name = "optimised-caching"
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none" # Don't include cookies in cache key
}
headers_config {
header_behavior = "none" # Don't include headers in cache key
}
query_strings_config {
query_string_behavior = "none" # Or whitelist specific params
}
}
}
Advanced: CloudFront Functions vs Response Header Policies
When to Use Response Header Policies
✅ Static header values (same for all requests) ✅ Simple caching rules ✅ No logic required ✅ Cost-sensitive (no execution cost)
When to Use CloudFront Functions
✅ Dynamic headers based on request ✅ Conditional logic (different headers per user/path) ✅ URL rewrites ✅ A/B testing
Example CloudFront Function:
function handler(event) {
var response = event.response;
var headers = response.headers;
// Dynamic cache control based on file extension
var uri = event.request.uri;
if (uri.endsWith('.js') || uri.endsWith('.css')) {
headers['cache-control'] = {
value: 'public, max-age=31536000, immutable'
};
} else if (uri.endsWith('.html')) {
headers['cache-control'] = {
value: 'public, max-age=3600, must-revalidate'
};
}
return response;
}
Performance Impact
Proper Cache-Control headers can dramatically improve your application:
Metrics from production (e-commerce site):
- ⬇️ 70% reduction in origin requests
- ⚡ 40% faster page load times
- 💰 $800/month savings in origin server costs
- 📈 2x improvement in Core Web Vitals (LCP)
Cost Savings Calculation:
Origin requests before: 10M requests/month
Origin requests after: 3M requests/month (70% cached)
AWS costs:
- CloudFront data transfer: Same
- Origin server: -50% (lower CPU/bandwidth)
- CloudFront requests: Same price
- Lambda@Edge: $0 (not using functions)
Total savings: ~$800/month for medium-traffic site
Security Considerations
Never Cache Sensitive Data
❌ WRONG - Caching authenticated API responses
Cache-Control: public, max-age=3600
✅ CORRECT - No caching for user data
Cache-Control: private, no-cache, no-store
Set Appropriate Headers for HTTPS Content
resource "aws_cloudfront_response_headers_policy" "security" {
name = "security-headers"
custom_headers_config {
items {
header = "Cache-Control"
value = "public, max-age=31536000"
override = true
}
}
security_headers_config {
strict_transport_security {
access_control_max_age_sec = 31536000
include_subdomains = true
preload = true
override = true
}
content_type_options {
override = true
}
frame_options {
frame_option = "DENY"
override = true
}
}
}
Monitoring and Validation
CloudWatch Metrics to Watch
# Cache hit ratio
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=E1234EXAMPLE \
--statistics Average \
--start-time 2025-01-01T00:00:00Z \
--end-time 2025-01-10T23:59:59Z \
--period 3600
Target Metrics:
- Cache Hit Ratio: >80% for static assets
- Origin Response Time: <200ms
- 4xx/5xx Error Rate: <1%
Testing in Different Environments
# Test production
curl -I https://d111111abcdef8.cloudfront.net/app.js
# Test with different regions (check edge location behavior)
curl -I https://d111111abcdef8.cloudfront.net/app.js \
--resolve d111111abcdef8.cloudfront.net:443:13.224.163.123
# Check cache status
curl -I https://d111111abcdef8.cloudfront.net/app.js | grep -i "x-cache"
Conclusion
Response Header Policies are the simplest and most cost-effective way to configure Cache-Control headers in CloudFront. Key takeaways:
- Use Response Header Policies for static cache rules (no code required)
- Override origin when you need consistent caching behavior
- Different policies for different content types (static vs dynamic)
- Monitor cache hit ratio to validate configuration
- Never cache user-specific or sensitive data
With proper caching configuration, you can achieve significant performance improvements and cost savings without writing a single line of code.
Additional Resources
- AWS CloudFront Response Headers Policies Documentation
- MDN: HTTP Caching
- Web.dev: HTTP Cache
- CloudFront Pricing
Questions or suggestions? Feel free to reach out on LinkedIn or GitHub.