Back to Blog
9 min read

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.

AWS CloudFront CDN Performance Caching

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 CDNs
  • private - Can only be cached by browsers, not CDNs
  • max-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 copy
  • no-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:

  1. Origin configuration - Requires backend changes
  2. CloudFront Functions - JavaScript code executed at edge
  3. 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

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:

  1. Select the behavior you want to modify (usually Default (*))
  2. Click “Edit”
  3. Scroll to Response headers policy
  4. Select your newly created policy
  5. 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-control header matches your policy
  • x-cache: Hit from cloudfront (indicates caching is working)
  • age header showing how long content has been cached

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
  • immutable prevents unnecessary revalidation
  • max-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 (3600 seconds)
  • must-revalidate ensures 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
  • private prevents CDN caching
  • no-store prevents 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 (2592000 seconds)
  • 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:

  1. Create separate Response Header Policies for each pattern
  2. Add ordered cache behaviors in your distribution
  3. Match path patterns to specific policies

Common Issues and Troubleshooting

Issue: Headers Not Appearing

Check:

  1. Distribution status is “Deployed”
  2. Wait 5-10 minutes after configuration change
  3. Clear browser cache (Ctrl+Shift+R or Cmd+Shift+R)
  4. Test with curl -I to bypass browser cache
  5. 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 rulesNo logic requiredCost-sensitive (no execution cost)

When to Use CloudFront Functions

Dynamic headers based on request ✅ Conditional logic (different headers per user/path) ✅ URL rewritesA/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:

  1. Use Response Header Policies for static cache rules (no code required)
  2. Override origin when you need consistent caching behavior
  3. Different policies for different content types (static vs dynamic)
  4. Monitor cache hit ratio to validate configuration
  5. 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


Questions or suggestions? Feel free to reach out on LinkedIn or GitHub.