· 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.

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:

  1. 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.
  2. Edge location: The request reaches the nearest point of presence. CloudFront looks for the resource in its local cache.
  3. Cache hit: If the resource is in cache and has not expired, CloudFront returns it directly. Minimal latency.
  4. 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).
  5. 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 to PriceClass_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=600

This 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:

FeatureCloudFront FunctionsLambda@Edge
RuntimeJavaScript (limited)Node.js, Python
Memory2 MB128 MB - 10 GB
Max duration1 ms5s (viewer) / 30s (origin)
EventsViewer request/responseViewer + origin request/response
Cost$0.10 per million$0.60 per million + duration
Network accessNoYes

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 TypeUncompressedgzipBrotliBrotli vs gzip Savings
HTML (100 KB)100 KB25 KB20 KB20% smaller
JavaScript (500 KB)500 KB120 KB95 KB21% smaller
CSS (80 KB)80 KB18 KB14 KB22% smaller
JSON (200 KB)200 KB45 KB36 KB20% 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:

  1. Data transfer to users: From $0.085/GB (first 10 TB/month) down to $0.020/GB (over 5 PB/month).
  2. HTTP/HTTPS requests: $0.0075 per 10,000 HTTPS requests (North America/Europe).
  3. Invalidations: First 1,000 paths/month free; then $0.005 per path.

Cost Scenarios

ScenarioTrafficRequestsMonthly Cost
Blog/landing page50 GB2M$5-8
Medium SaaS application500 GB20M$50-70
E-commerce with heavy assets5 TB100M$450-550
Streaming platform50 TB500M$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 Average

CloudFront 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.

Back to Blog

Related Posts

View All Posts »