Skip to content
CloudFront CDN: Setup and Optimization Guide

CloudFront CDN: Setup and Optimization Guide

DodaTech Updated Jun 20, 2026 7 min read

AWS CloudFront is a content delivery network (CDN) that delivers data, video, and applications to users globally with low latency and high transfer speeds — using 600+ edge locations worldwide.

What You’ll Learn

  • Setting up a CloudFront distribution with custom origins
  • Configuring caching policies, behaviors, and TTLs
  • Securing content with SSL/TLS, WAF, and signed URLs
  • Customizing content with Lambda@Edge
  • Monitoring, logging, and cost optimization

Why CloudFront Matters

Serving content from a single region means users in other continents wait 200-500ms for every request. CloudFront caches content at 600+ edge locations, reducing latency to under 20ms for most users. It also reduces origin load (fewer requests hit your servers) and provides DDoS protection. DodaTech uses CloudFront to distribute Durga Antivirus Pro signature updates — users in 190+ countries download updates from their nearest edge location, reducing download times by 80%.

    flowchart LR
    A[AWS Fundamentals] --> B[CloudFront CDN]
    B --> C[Distributions]
    B --> D[Origins]
    B --> E[Caching]
    B --> F[Security]
    C --> G[Edge Locations]
    D --> H[S3 / ALB / Custom]
    E --> I[TTL Policies]
    F --> J[WAF / Signed URLs]
    style B fill:#f90,color:#fff
  
Prerequisites: Basic AWS knowledge. Familiarity with S3 and Linux command line.

Setting Up a Distribution

Distribution with S3 Origin

# Create CloudFront distribution for S3 bucket
aws cloudfront create-distribution \
  --origin-domain-name dodatech-site.s3.us-east-1.amazonaws.com \
  --default-root-object index.html \
  --enabled \
  --comment "DodaTech website"

# Output:
# {
#     "Distribution": {
#         "Id": "E1XAMPLE12345",
#         "DomainName": "d1234example.cloudfront.net",
#         "Status": "InProgress"
#     }
# }

Terraform Configuration

# cloudfront.tf
resource "aws_cloudfront_distribution" "site" {
  origin {
    domain_name = aws_s3_bucket.site.bucket_regional_domain_name
    origin_id   = "S3-site"

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.oai.cloudfront_access_identity_path
    }
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"  # Only US/Europe (cheaper)

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-site"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600   # 1 hour
    max_ttl                = 86400  # 1 day
    compress               = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  tags = {
    Name        = "dodatech-site"
    Environment = "production"
  }
}

Caching Policies

Cache Behavior Routing

Route different paths to different origins with different TTLs:

# API behavior — no cache, forward all headers
ordered_cache_behavior {
  path_pattern     = "/api/*"
  target_origin_id = "ALB-origin"

  allowed_methods  = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
  cached_methods   = ["GET", "HEAD"]

  forwarded_values {
    query_string = true
    headers      = ["Authorization", "Content-Type"]
    cookies {
      forward = "all"
    }
  }

  viewer_protocol_policy = "https-only"
  min_ttl                = 0
  default_ttl            = 0
  max_ttl                = 0
}

# Static assets — long cache, compress
ordered_cache_behavior {
  path_pattern     = "/static/*"
  target_origin_id = "S3-site"

  viewer_protocol_policy = "redirect-to-https"
  default_ttl            = 2592000  # 30 days
  compress               = true
}

Custom Error Responses

# Show custom error pages instead of generic CloudFront errors
custom_error_response {
  error_code         = 403
  response_code      = 200
  response_page_path = "/error.html"  # SPA fallback
}

custom_error_response {
  error_code         = 404
  response_code      = 200
  response_page_path = "/error.html"
}

Security: WAF and Signed URLs

WAF Integration

resource "aws_wafv2_web_acl" "cloudfront" {
  name        = "cloudfront-waf"
  scope       = "CLOUDFRONT"

  default_action {
    allow {}
  }

  rule {
    name     = "rate-limit"
    priority = 1

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 5000
        aggregate_key_type = "IP"
      }
    }
  }

  rule {
    name     = "block-bad-ips"
    priority = 2

    action {
      block {}
    }

    statement {
      ip_set_reference_statement {
        arn = aws_wafv2_ip_set.blocked.arn
      }
    }
  }
}

resource "aws_cloudfront_distribution" "site" {
  web_acl_id = aws_wafv2_web_acl.cloudfront.arn
  # ...
}

Signed URLs for Private Content

# generate_signed_url.py
import boto3
from datetime import datetime, timedelta

def generate_signed_url(distribution_url, private_key_path, key_pair_id, expire_hours=24):
    signer = boto3.client('cloudfront')
    url = f"{distribution_url}/private/signatures-v2.db"
    expiry = datetime.now() + timedelta(hours=expire_hours)

    signed_url = signer.sign_url(
        url=url,
        keypair_id=key_pair_id,
        private_key=private_key_path,
        expire_time=expiry
    )
    return signed_url

url = generate_signed_url(
    "https://d1234example.cloudfront.net",
    "/path/to/private-key.pem",
    "KEXAMPLE12345"
)
print(f"Signed URL (expires in 24h):\n{url}")

# Output:
# https://d1234example.cloudfront.net/private/signatures-v2.db?...
#   Expires=1718899200&Signature=abc123...&Key-Pair-Id=KEXAMPLE12345

Lambda@Edge

Customize content at edge locations:

# lambda_edge_viewer_request.py
# Redirect mobile users to mobile site
import json

def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']

    # Check User-Agent for mobile
    if 'user-agent' in headers:
        ua = headers['user-agent'][0]['value'].lower()
        if any(w in ua for w in ['mobile', 'android', 'iphone']):
            response = {
                'status': '302',
                'statusDescription': 'Found',
                'headers': {
                    'location': [{
                        'key': 'Location',
                        'value': f"https://m.example.com{request['uri']}"
                    }]
                }
            }
            return response
    return request

Logging and Monitoring

# Enable CloudFront logging
resource "aws_cloudfront_distribution" "site" {
  logging_config {
    include_cookies = false
    bucket          = aws_s3_bucket.logs.bucket_domain_name
    prefix          = "cloudfront/"
  }
}
# CloudFront metrics in CloudWatch / Prometheus
# Total requests per distribution
aws_cloudfront_requests_sum{distribution_id="E1XAMPLE12345"}

# Error rate (4xx + 5xx)
aws_cloudfront_error_rate_sum{distribution_id="E1XAMPLE12345"}

# Data transfer in GB
aws_cloudfront_bytes_downloaded_sum{distribution_id="E1XAMPLE12345"}

Cost Optimization

StrategySavingsImplementation
Price Class30-50%Use PriceClass_100 (US+Europe) instead of PriceClass_All
Compression30-60% bandwidthEnable compress = true for text content
Cache hit ratio50-80% origin offloadIncrease TTLs, use cache-control headers
S3 origin access0 data transfer costUse OAI to keep S3 private

Common Mistakes

  1. No cache invalidation strategy: When content changes, old cached versions serve stale data. Use versioned filenames (style.v2.css) instead of invalidating. Invalidation costs $0.005/path and takes 5-15 minutes.

  2. Not enabling compression: Text files (HTML, CSS, JS) compress 60-80%. Without compression, users download larger files and bandwidth costs increase. Enable compress = true.

  3. Using all edge locations unnecessarily: If your users are only in the US and Europe, use PriceClass_100. Including Asia, South America, and Australia increases cost 2x without benefit.

  4. Serving private content without signed URLs: Without signed URLs, any user who guesses a CloudFront URL can access private content. Always use signed URLs or cookies for authenticated content.

  5. No custom error pages: CloudFront’s default error pages are ugly and don’t match your brand. Configure custom error responses that redirect to your own error pages.

Practice Questions

  1. What is the difference between CloudFront and S3? Answer: S3 is object storage. CloudFront is a CDN that caches content at edge locations. CloudFront sits in front of S3 (or other origins) to serve content with low latency globally.

  2. How does CloudFront handle cache invalidation? Answer: You can invalidate specific paths via console, CLI, or API. Invalidations cost $0.005 per path and take 5-15 minutes to propagate. Better approach: use versioned filenames.

  3. What is Origin Access Identity (OAI)? Answer: OAI is a virtual identity that CloudFront uses to access your S3 bucket. With OAI, only CloudFront can access the bucket — users can’t bypass the CDN and access S3 directly.

  4. What is Lambda@Edge used for? Answer: Lambda@Edge runs code at CloudFront edge locations in response to viewer or origin requests/responses. Use cases: A/B testing, URL rewrites, authentication, header modification.

Challenge

Set up a production CloudFront distribution: create an S3 bucket with static website content, configure CloudFront with OAI, add a custom domain via Route 53, enable HTTPS with ACM certificate, set up WAF with rate limiting and IP blocking, configure API behavior to forward to ALB, and optimize caching with versioned static assets.

FAQ

How much does CloudFront cost?
: Pay per GB transferred ($0.085/GB US, higher for other regions) and per request ($0.0075-0.0120 per 10,000 requests). Price class options reduce cost.
Can CloudFront serve dynamic content?
: Yes. CloudFront can forward API requests to a custom origin (ALB, EC2, Lambda) with no caching. It still benefits from persistent connections and edge termination.
What is the difference between CloudFront and Route 53?
: Route 53 is DNS — it resolves domain names to IPs. CloudFront is a CDN — it serves cached content from edge locations. They often work together: Route 53 points to CloudFront.
Does CloudFront support WebSockets?
: Yes, since 2021. WebSocket connections terminate at the edge and are proxied to the origin. No caching for WebSocket traffic.
How do I handle SPA routing with CloudFront?
: Configure a custom error response to return index.html for 403/404 errors. This enables client-side routing. Set up separate cache behavior for /api/*.

What’s Next

TopicDescription
AWS ECS
Running containers at scale
Amazon EKS
Managed Kubernetes on AWS

Related topics: AWS, S3, CloudFront

What’s Next

Congratulations on completing this CloudFront tutorial! Here’s where to go from here:

  • Practice daily — Review your current CDN setup for optimization
  • Build a project — Deploy a static site behind CloudFront with HTTPS
  • Explore related topics — Check out ECS and EKS for container orchestration

Remember: every expert was once a beginner. Keep coding!

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro