CloudFront and CDN Delivery

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour on this page are current for AWS CLI v2 (2.37 line), the CloudFront API version 2020-05-31 and the CloudFront flat-rate pricing plans launched in November 2025 (checked 2026-10-01); re-verify against aws --version and the linked reference pages before relying on them in a pipeline. Examples use the placeholder account ID 111122223333, the placeholder distribution ID E1EXAMPLE1234, the placeholder domain example.com and the Region us-east-1; all are stand-ins for your own values.

Amazon CloudFront is AWS’s content delivery network (CDN): a global network of edge locations that caches your content close to viewers, terminates TLS there, and fetches from your origin (an S3 bucket, a load balancer, an API) only when it has to. For Bookshelf it is the front door of the whole application: the static web front end comes from the private bucket bookshelf-web-<account-id> and requests under /api/* are forwarded to the alb-bookshelf Application Load Balancer in front of catalog-api and orders-api, so the browser sees a single HTTPS origin, one certificate and no CORS configuration. This page covers the object model, the recommended private S3 pattern with Origin Access Control (OAC), the three policy types that control caching and forwarding, custom domains, invalidation, edge compute, private content, security, logging, the pay-as-you-go and flat-rate pricing models, and troubleshooting. The bucket itself, its Block Public Access settings and the other ways of securing it are on S3 Security and Static Hosting; storage classes and uploads are on S3 Object Storage. Certificates and DNS are on Route 53, DNS and ACM Certificates and the load balancer on Load Balancers and Gateways. A React build deployed this way is shown on Build and Deployment. For the Azure counterpart see Front Door and CDN.

Concepts

Distribution

The unit you create and pay attention to. It has a domain name of the form dxxxxxxxxxxxx.cloudfront.net, optional alternate domain names (your own hostnames), a TLS certificate, a set of origins and an ordered list of behaviors. Changes are versioned by an ETag and take a few minutes to deploy to every edge location.

Origin

Where CloudFront fetches content on a cache miss: an S3 bucket (through its Region-specific REST endpoint), a custom HTTP origin (an ALB, API Gateway, a Lambda function URL, any public server), a VPC origin (a private ALB, NLB or EC2 instance), or a Media service endpoint. An origin group pairs a primary and a secondary origin for failover on selected status codes.

Behavior (cache behavior)

A path pattern such as /api/ or .js plus everything that applies to matching requests: the target origin, the allowed HTTP methods, the viewer protocol policy, the cache policy, the origin request policy, the response headers policy, edge function associations, signed-URL requirements and compression. Patterns are evaluated in order and the Default (*) behavior catches the rest.

Policies

Reusable, named configuration objects attached to behaviors. A cache policy defines the TTLs and the cache key; an origin request policy defines what extra headers, cookies and query strings are forwarded to the origin on a miss without changing the cache key; a response headers policy adds or removes response headers (security headers, CORS). They replace the legacy per-behavior ForwardedValues settings.

Origin access control (OAC)

The mechanism that lets CloudFront sign its requests to an S3 origin (and to Lambda function URLs and some media origins) so the origin can stay private.

Edge location, regional edge cache, Origin Shield

The cache layers a request can hit. The geography is covered on AWS Global Infrastructure.

Diagram of a viewer request reaching a CloudFront edge location
flowchart TD D["Distribution bookshelf-cdn"] --> AN["Alternate domain names + ACM certificate"] D --> WAF["Web ACL (optional)"] D --> O1["Origin s3-web: bookshelf-web bucket"] D --> O2["Origin alb-api: alb-bookshelf"] O1 --> OAC["Origin access control"] D --> B0["Default behavior *"] D --> B1["Behavior /api/*"] B0 --> O1 B1 --> O2 B0 --> CP1["Cache policy CachingOptimized"] B0 --> RH["Response headers policy"] B0 --> FN["CloudFront Function viewer-request"] B1 --> CP2["Cache policy CachingDisabled"] B1 --> OR["Origin request policy AllViewer"]

Request flow in one paragraph

A viewer resolves www.example.com (a Route 53 alias to the distribution) and opens HTTPS to the nearest edge location. CloudFront picks the first behavior whose path pattern matches, runs any viewer-request function, computes the cache key and looks it up. On a hit the object is returned immediately. On a miss the request moves to the regional edge cache and, if enabled, Origin Shield; only after both miss does CloudFront contact the origin, signed by the OAC for S3. The response is stored at each layer according to the TTL rules and returned, with any response headers policy applied. Concurrent misses for the same key are collapsed into one origin request.

Prerequisites for Bookshelf

  • The private bucket bookshelf-web-<account-id> with Block Public Access on, ACLs disabled (Object Ownership Bucket owner enforced, the default for new buckets) and the compiled front end uploaded; see S3 Security and Static Hosting. Use the bucket’s REST endpoint (bookshelf-web-<account-id>.s3.us-east-1.amazonaws.com), never the S3 website endpoint.

  • The Application Load Balancer alb-bookshelf with an HTTPS listener whose certificate covers the origin hostname you give CloudFront (for example origin.example.com); see Load Balancers and Gateways.

  • A public ACM certificate for www.example.com requested in us-east-1, because CloudFront only reads certificates from that Region regardless of where your workload runs (see Custom domains, HTTPS and Route 53).

  • A Route 53 public hosted zone for example.com (or any DNS provider; Route 53 gives you alias records).

export AWS_REGION=us-east-1
export ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export BUCKET="bookshelf-web-${ACCOUNT_ID}"
export DOMAIN=www.example.com
export ORIGIN_DOMAIN=origin.example.com   # DNS name of alb-bookshelf, covered by its certificate

CloudFront is a global service: most commands do not take a Region, the API endpoint is in us-east-1, and the metrics and logs delivery APIs live there too. Console links go to the CloudFront console and the Region selector does not matter, except when you create the ACM certificate and the AWS WAF web ACL, which must be in N. Virginia.

Origin access control for a private S3 bucket

The recommended pattern for a static site is a private bucket that only CloudFront may read. CloudFront authenticates to S3 with Origin Access Control (OAC): it signs each origin request with Signature Version 4 on behalf of the service principal cloudfront.amazonaws.com, and the bucket policy trusts that principal only when the request comes from your distribution (AWS:SourceArn). Older guides and tutorials still show a public bucket configured as a website, or a CloudFront origin access identity (OAI). Both are outdated: OAI is the legacy mechanism, does not support SSE-KMS encrypted objects, dynamic PUT or DELETE requests, or S3 buckets in every Region (including Regions opened after December 2022), and is not available on flat-rate plans, so new work uses OAC. Migration from an OAI is covered below.

Diagram showing a viewer reaching a private S3 bucket only through a CloudFront distribution whose origin access control signs requests

Rules to remember:

  • The origin must be the S3 REST endpoint. A bucket configured as a website endpoint is an ordinary custom HTTP origin; it cannot use OAC (or OAI), which is why it must be public. Website endpoints also lack HTTPS.

  • Set the OAC signing behavior to always (console: Sign requests (recommended)). The alternatives no-override (sign only if the viewer sent no Authorization header) and never (turn signing off) exist for special cases.

  • Object Ownership must be Bucket owner enforced, the default for new buckets; choose Bucket owner preferred only if you must keep using ACLs for objects uploaded through CloudFront.

  • An S3 Multi-Region Access Point origin needs a different OAC configuration; see the OAC documentation.

In the AWS Management Console

  1. Open the CloudFront console → Security → Origin access → Create control setting.

  2. Enter the name bookshelf-web-oac, leave Sign requests (recommended) selected, choose S3 as the origin type and select Create.

  3. Attach the OAC while creating the distribution (see Creating the distribution) by choosing the S3 origin and Origin access control settings (recommended). The console offers to Copy policy; after the distribution exists, open the bucket in S3 → Permissions → Bucket policy → Edit and paste it, or let the console add it for you where it offers a button.

With the AWS CLI

aws cloudfront create-origin-access-control --origin-access-control-config \
  Name=bookshelf-web-oac,Description="Bookshelf static site",SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=s3 \
  --query 'OriginAccessControl.Id' --output text

Capture the returned ID in OAC_ID. Derived from aws cloudfront create-origin-access-control and Restrict access to an Amazon S3 origin.

The bucket policy

Attach this policy to the bucket after the distribution exists (it needs the distribution ID). It allows reads only for requests made on behalf of that one distribution; save it as bucket-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipalReadOnly",
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::bookshelf-web-111122223333/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE1234"
        }
      }
    }
  ]
}
aws s3api put-bucket-policy --bucket "$BUCKET" --policy file://bucket-policy.json

Two refinements matter in practice:

  • To let CloudFront return a clean 404 for missing keys instead of 403, also grant s3:ListBucket on the bucket ARN (not the /* ARN) with the same condition. Without it, S3 answers 403 AccessDenied for a key that does not exist, because revealing absence would leak information. The single-page-application pattern below turns both into index.html, so Bookshelf can live without it.

  • If the objects use SSE-KMS with a customer managed key, add a statement to the key policy so the distribution may decrypt them (without it every request fails with 403):

{
  "Sid": "AllowCloudFrontServicePrincipalSSEKMS",
  "Effect": "Allow",
  "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey*"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE1234"
    }
  }
}

Key policies are explained on Secrets Manager, Parameter Store and KMS. If you need CloudFront to write to the bucket (uploads through the distribution), add s3:PutObject to the bucket policy statement and allow the PUT method on the behavior.

Migrating from an OAI

Add a second statement for the legacy principal next to the OAC statement so access never lapses, switch the origin to the OAC (edit the origin, set Origin access to the OAC, remove the OAI), wait for the distribution to deploy, and then delete the OAI statement. The documented migration policy uses a principal of the form arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <identity-ID> alongside the service-principal statement shown above. With the CLI, fetch the configuration (get-distribution-config), put the OAC ID into OriginAccessControlId, empty OriginAccessIdentity, rename ETag to IfMatch, and send it back with update-distribution.

Creating the distribution

In the AWS Management Console

  1. Open the CloudFront console → Create distribution. The wizard first asks you to choose a pricing plan (Free, Pro, Business, Premium or pay-as-you-go); for learning, the default pay-as-you-go standard distribution gives access to every feature on this page. See Pricing model before choosing.

  2. Origin: for Origin type choose Amazon S3 and select the bucket bookshelf-web-<account-id> (the REST endpoint). For Origin access choose Origin access control settings (recommended) and select bookshelf-web-oac. Leave Origin Shield off for now.

  3. Default cache behavior: Viewer protocol policy Redirect HTTP to HTTPS; Allowed HTTP methods GET, HEAD; tick Compress objects automatically; Cache key and origin requests → Cache policy and origin request policy → cache policy CachingOptimized; Response headers policy SecurityHeadersPolicy.

  4. Settings: add www.example.com as an Alternate domain name (CNAME), choose the us-east-1 ACM certificate, set the Security policy (see HTTPS and security policies), Default root object index.html, and leave HTTP/2 and HTTP/3 and IPv6 on. Pick a Price class (see Pricing model).

  5. Select Create distribution. Copy the Distribution ARN shown in the OAC banner, paste the bucket policy (the console offers Copy policy next to the origin) and wait until Last modified changes from Deploying to a date.

  6. Add the second origin and behavior: Origins → Create origin with Origin domain origin.example.com, Protocol HTTPS only; then Behaviors → Create behavior with Path pattern /api/, that origin, *Viewer protocol policy Redirect HTTP to HTTPS, Allowed HTTP methods GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE, cache policy CachingDisabled and origin request policy AllViewer.

  7. Error pages → Create custom error response for HTTP 403 and 404, Response page path /index.html, HTTP response code 200, so deep links of the single-page application work.

With the AWS CLI

create-distribution takes the whole configuration as one document. The file below describes the Bookshelf front end: an S3 origin with the OAC, the alb-bookshelf origin, the default behavior for static content with managed policies, an /api/* behavior for the API, an SPA fallback and a custom domain. Save it as distribution-config.json after substituting the angle-bracket values:

{
  "CallerReference": "bookshelf-cdn-2026-10-01",
  "Comment": "bookshelf-cdn",
  "Enabled": true,
  "DefaultRootObject": "index.html",
  "HttpVersion": "http2and3",
  "IsIPV6Enabled": true,
  "PriceClass": "PriceClass_100",
  "Aliases": { "Quantity": 1, "Items": ["www.example.com"] },
  "ViewerCertificate": {
    "ACMCertificateArn": "arn:aws:acm:us-east-1:111122223333:certificate/<certificate-id>",
    "SSLSupportMethod": "sni-only",
    "MinimumProtocolVersion": "TLSv1.2_2021"
  },
  "Origins": {
    "Quantity": 2,
    "Items": [
      {
        "Id": "s3-web",
        "DomainName": "bookshelf-web-111122223333.s3.us-east-1.amazonaws.com",
        "OriginAccessControlId": "<OAC_ID>",
        "S3OriginConfig": { "OriginAccessIdentity": "" }
      },
      {
        "Id": "alb-api",
        "DomainName": "origin.example.com",
        "CustomOriginConfig": {
          "HTTPPort": 80,
          "HTTPSPort": 443,
          "OriginProtocolPolicy": "https-only",
          "OriginSslProtocols": { "Quantity": 1, "Items": ["TLSv1.2"] },
          "OriginReadTimeout": 30,
          "OriginKeepaliveTimeout": 5
        }
      }
    ]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "s3-web",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": {
      "Quantity": 2, "Items": ["GET", "HEAD"],
      "CachedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] }
    },
    "Compress": true,
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "ResponseHeadersPolicyId": "67f7725c-6f97-4210-82d7-5512b31e9d03"
  },
  "CacheBehaviors": {
    "Quantity": 1,
    "Items": [
      {
        "PathPattern": "/api/*",
        "TargetOriginId": "alb-api",
        "ViewerProtocolPolicy": "redirect-to-https",
        "AllowedMethods": {
          "Quantity": 7,
          "Items": ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"],
          "CachedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] }
        },
        "Compress": true,
        "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
        "OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3"
      }
    ]
  },
  "CustomErrorResponses": {
    "Quantity": 2,
    "Items": [
      { "ErrorCode": 403, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 10 },
      { "ErrorCode": 404, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 10 }
    ]
  }
}

The three UUIDs are the AWS managed policies CachingOptimized, SecurityHeadersPolicy and CachingDisabled, plus AllViewer for the origin request policy; they are identical in every account (see The three policy types). PriceClass_100 serves from the cheapest group of edge locations (North America and Europe); omit it for global delivery. Because the /api/* behavior uses AllViewer, the viewer’s Host header is forwarded to the ALB; use AllViewerExceptHostHeader instead for API Gateway and Lambda function URL origins, which expect their own host name.

aws cloudfront create-distribution --distribution-config file://distribution-config.json \
  --query 'Distribution.[Id,DomainName,ARN]' --output text

export DIST_ID=E1EXAMPLE1234   # use the Id returned above
aws cloudfront wait distribution-deployed --id "$DIST_ID"

# Now that the ARN is known, fill it into bucket-policy.json and apply it
aws s3api put-bucket-policy --bucket "$BUCKET" --policy file://bucket-policy.json

Derived from aws cloudfront create-distribution, aws cloudfront wait distribution-deployed and All distribution settings reference. A distribution that has no WAF web ACL, custom domain or certificate is just as valid; remove Aliases and ViewerCertificate to test with the cloudfront.net name first. The ETag returned by get-distribution is the --if-match value for every later update-distribution call.

With CloudFormation

CloudFormation is the repeatable way to keep the OAC, the distribution and the bucket policy together. This template creates all three for an existing bucket (the policy references the distribution it is paired with, so there is no manual ID copying):

AWSTemplateFormatVersion: "2010-09-09"
Description: Bookshelf CDN - private S3 origin with OAC, ALB origin for /api/*
Parameters:
  BucketName:     { Type: String, Default: bookshelf-web-111122223333 }
  DomainName:     { Type: String, Default: www.example.com }
  CertificateArn: { Type: String, Description: ACM certificate in us-east-1 }
  OriginDomain:   { Type: String, Default: origin.example.com }
Resources:
  Oac:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: bookshelf-web-oac
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
  Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Comment: bookshelf-cdn
        Enabled: true
        DefaultRootObject: index.html
        HttpVersion: http2and3
        IPV6Enabled: true
        PriceClass: PriceClass_100
        Aliases: [!Ref DomainName]
        ViewerCertificate:
          AcmCertificateArn: !Ref CertificateArn
          SslSupportMethod: sni-only
          MinimumProtocolVersion: TLSv1.2_2021
        Origins:
          - Id: s3-web
            DomainName: !Sub "${BucketName}.s3.${AWS::Region}.amazonaws.com"
            OriginAccessControlId: !GetAtt Oac.Id
            S3OriginConfig: { OriginAccessIdentity: "" }
          - Id: alb-api
            DomainName: !Ref OriginDomain
            CustomOriginConfig:
              OriginProtocolPolicy: https-only
              OriginSSLProtocols: [TLSv1.2]
        DefaultCacheBehavior:
          TargetOriginId: s3-web
          ViewerProtocolPolicy: redirect-to-https
          Compress: true
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6          # CachingOptimized
          ResponseHeadersPolicyId: 67f7725c-6f97-4210-82d7-5512b31e9d03 # SecurityHeadersPolicy
        CacheBehaviors:
          - PathPattern: /api/*
            TargetOriginId: alb-api
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
            Compress: true
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad          # CachingDisabled
            OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3  # AllViewer
        CustomErrorResponses:
          - { ErrorCode: 403, ResponseCode: 200, ResponsePagePath: /index.html, ErrorCachingMinTTL: 10 }
          - { ErrorCode: 404, ResponseCode: 200, ResponsePagePath: /index.html, ErrorCachingMinTTL: 10 }
  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref BucketName
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AllowCloudFrontServicePrincipalReadOnly
            Effect: Allow
            Principal: { Service: cloudfront.amazonaws.com }
            Action: s3:GetObject
            Resource: !Sub "arn:${AWS::Partition}:s3:::${BucketName}/*"
            Condition:
              StringEquals:
                "AWS:SourceArn": !Sub "arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}"
Outputs:
  DistributionId:     { Value: !Ref Distribution }
  DistributionDomain: { Value: !GetAtt Distribution.DomainName }
aws cloudformation deploy --template-file cdn.yaml --stack-name bookshelf-cdn \
  --parameter-overrides CertificateArn="arn:aws:acm:us-east-1:111122223333:certificate/<certificate-id>"

Derived from the AWS::CloudFront::Distribution, AWS::CloudFront::OriginAccessControl and aws cloudformation deploy references. The stack can be deployed in any Region as long as the certificate is from us-east-1; see Infrastructure as Code for stacks, change sets and drift detection.

With the AWS CDK (Java)

The CDK L2 construct S3BucketOrigin.withOriginAccessControl creates the OAC and the bucket policy for you:

import software.amazon.awscdk.services.cloudfront.*;
import software.amazon.awscdk.services.cloudfront.origins.*;
import software.amazon.awscdk.services.s3.Bucket;

Bucket web = Bucket.Builder.create(this, "Web").build(); // private by default

Distribution cdn = Distribution.Builder.create(this, "BookshelfCdn")
    .defaultRootObject("index.html")
    .defaultBehavior(BehaviorOptions.builder()
        .origin(S3BucketOrigin.withOriginAccessControl(web))
        .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
        .cachePolicy(CachePolicy.CACHING_OPTIMIZED)
        .responseHeadersPolicy(ResponseHeadersPolicy.SECURITY_HEADERS)
        .build())
    .additionalBehaviors(java.util.Map.of("/api/*", BehaviorOptions.builder()
        .origin(HttpOrigin.Builder.create("origin.example.com")
            .protocolPolicy(OriginProtocolPolicy.HTTPS_ONLY).build())
        .allowedMethods(AllowedMethods.ALLOW_ALL)
        .cachePolicy(CachePolicy.CACHING_DISABLED)
        .originRequestPolicy(OriginRequestPolicy.ALL_VIEWER)
        .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
        .build()))
    .build();

Derived from the aws-cdk-lib.aws_cloudfront module documentation; CDK basics are on Infrastructure as Code.

The three policy types

A behavior controls caching and forwarding through three independent, reusable policies. Use the managed ones unless you have a reason not to; attach them by name in the console and by ID in the CLI, CloudFormation and SDKs. The IDs below are the current published values (checked 2026-10-01).

Managed policy ID Use it for

Cache policy CachingOptimized

658327ea-f89d-4fab-a63d-7e88639e58f6

Static assets. Minimum TTL 1 second, default 24 hours, maximum 365 days; cache key is the normalized Accept-Encoding only (no cookies, headers or query strings); Gzip and Brotli on.

Cache policy CachingDisabled

4135ea2d-6df8-44a3-9df3-4b5a84be39ad

Dynamic or non-cacheable content such as the API: all TTLs 0, nothing in the cache key.

Cache policy UseOriginCacheControlHeaders

83da9c7e-98b4-4e11-a168-04f0df8e2c65

Origins that send correct Cache-Control headers: minimum and default TTL 0, so the origin decides. The UseOriginCacheControlHeaders-QueryStrings variant (4cc15a8a-d715-48a4-82b8-cc0b614638fe) also keys on all query strings.

Origin request policy AllViewer

216adef6-5c7f-47e4-b989-5492eafa07d3

Forward every viewer header, cookie and query string to the origin on a miss.

Origin request policy AllViewerExceptHostHeader

b689b0a8-53d0-40ab-baf2-68738e2966ac

API Gateway and Lambda function URL origins, which need the origin’s own Host.

Origin request policy CORS-S3Origin

88a5eaf4-2fd4-4709-b370-b4c650ea3fcf

Forward Origin and the preflight headers to S3 so S3 CORS rules work through the cache.

Response headers policy SecurityHeadersPolicy

67f7725c-6f97-4210-82d7-5512b31e9d03

Adds Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy and X-XSS-Protection; the origin’s own value wins except for nosniff.

Response headers policy CORS-with-preflight-and-SecurityHeadersPolicy

eaab4381-ed33-4a86-88ca-d9558dc6cd63

Security headers plus permissive CORS including OPTIONS preflight; SimpleCORS is 60669652-455b-4ae9-85a4-c4c02393f86c.

Cache policies with a minimum TTL above 0 (such as CachingOptimized) cache an object for at least that long even if the origin says no-store or private, so never attach them to personalised responses. The CORS policy IDs are convenient for a quick start; for production prefer a custom policy listing your real origins.

Creating your own policies

A custom cache policy is the right tool when the cache key must include something specific, such as a language cookie or a v query string, while ignoring everything else (which keeps the hit ratio high):

cat > cache-policy.json <<'EOF'
{
  "Name": "bookshelf-api-catalog",
  "Comment": "Cache GET /api/catalog by query string and Accept-Language",
  "MinTTL": 0,
  "DefaultTTL": 60,
  "MaxTTL": 300,
  "ParametersInCacheKeyAndForwardedToOrigin": {
    "EnableAcceptEncodingGzip": true,
    "EnableAcceptEncodingBrotli": true,
    "HeadersConfig": { "HeaderBehavior": "whitelist", "Headers": { "Quantity": 1, "Items": ["Accept-Language"] } },
    "CookiesConfig": { "CookieBehavior": "none" },
    "QueryStringsConfig": { "QueryStringBehavior": "all" }
  }
}
EOF
aws cloudfront create-cache-policy --cache-policy-config file://cache-policy.json \
  --query 'CachePolicy.Id' --output text

Derived from aws cloudfront create-cache-policy. In the console use Policies → Cache → Create cache policy; origin request and response headers policies are on neighbouring tabs (create-origin-request-policy, create-response-headers-policy). The reference pages are Control the cache key with a policy, Control origin requests with a policy and Add HTTP response headers. Older guides and tutorials configure caching through the per-behavior ForwardedValues and MinTTL fields; those legacy settings still exist in the API but are not supported on flat-rate plans, and a behavior uses either them or the policies, never both.

How caching behaves

Time to live

For each cached response CloudFront computes how long it may be served without asking the origin again:

  • If the origin sends Cache-Control: s-maxage=N or max-age=N, or an Expires header, CloudFront uses that value (s-maxage takes precedence over max-age for shared caches, and both over Expires), clamped between the cache policy’s minimum and maximum TTL.

  • With no caching header, the policy’s default TTL applies.

  • Cache-Control: no-cache, no-store or private cause the object not to be cached when the minimum TTL is 0 (viewer-side revalidation still works through ETag and If-None-Match).

  • Error responses from the origin are cached for the error caching minimum TTL (10 seconds by default; the custom error response setting changes it).

flowchart TD A["Origin response arrives"] --> B{"Cache-Control no-store, no-cache or private?"} B -- "yes and min TTL is 0" --> N["Not cached"] B -- "no" --> C{"s-maxage, max-age or Expires present?"} C -- "yes" --> D["Use that lifetime"] C -- "no" --> E["Use default TTL"] D --> F["Clamp between min TTL and max TTL"] E --> F F --> G["Serve from cache until expiry, then revalidate with the origin"]

The cache key, compression and Origin Shield

  • The cache key decides whether two requests share a cached object. By default it is the hostname and path; the cache policy adds the headers, cookies and query strings you opt into. A key that is too broad (all cookies, all query strings) shreds the hit ratio; a key that is too narrow serves one user another user’s response. Forward something to the origin without caching on it by putting it in the origin request policy only.

  • Compression: with Compress objects automatically and a cache policy that accepts Accept-Encoding, CloudFront compresses cacheable text types in Gzip or Brotli at the edge; the origin needs no support. Objects must be within the documented size range and send a Content-Length.

  • Origin Shield is an optional extra cache layer in the single AWS Region you choose (close to the origin). Every miss from the regional caches funnels through it, which increases the hit ratio, lowers origin load and collapses duplicate requests; it is billed per request, so enable it for origins that are expensive or slow, not for S3.

  • Diagnosing a hit or miss: the X-Cache response header reads Hit from cloudfront, RefreshHit from cloudfront (expired object revalidated), Miss from cloudfront, Error from cloudfront or Redirect from cloudfront; Age is the seconds the object has been cached, X-Amz-Cf-Pop names the edge location and X-Amz-Cf-Id is the request ID to quote to AWS Support.

curl -sI https://www.example.com/assets/app.3f9c1a.js | grep -iE 'x-cache|age|x-amz-cf-pop|cache-control'

Updating content: invalidation and versioned names

The cheapest and most reliable way to ship a new front-end release is to give files content-hashed names (app.3f9c1a.js, which Vite and webpack do by default), upload them with Cache-Control: public, max-age=31536000, immutable, and upload only index.html with a short lifetime (no-cache or a minute). Nothing then needs to be invalidated except /index.html. When you do need to remove content from every cache, create an invalidation.

In the AWS Management Console

  1. Open CloudFront → the distribution → Invalidations → Create invalidation.

  2. Enter one path per line, for example /index.html or /images/, and select *Create invalidation.

  3. Watch the status change from In progress to Completed; it normally takes a minute or two.

With the AWS CLI

aws s3 sync dist/ "s3://${BUCKET}/" --delete \
  --exclude index.html --cache-control "public,max-age=31536000,immutable"
aws s3 cp dist/index.html "s3://${BUCKET}/index.html" --cache-control "no-cache"

aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "/index.html"
aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "/images/*"   # one wildcard path
  • Cost: the first 1,000 invalidation paths per month are free across all distributions in the account; after that each path is charged. A path with a wildcard is one path no matter how many objects it covers, and a batch of many paths is billed per path. Versioned names cost nothing.

  • Invalidation by cache tag (introduced in 2026): have the origin return a comma-separated list of tags in a header (for S3, store object metadata and set the distribution’s CacheTagConfig.HeaderName to x-amz-meta-cache-tag), then invalidate with a path starting with #. One tag counts as one path against the free allowance, and tag invalidation is available on the Pro, Business and Premium flat-rate plans.

aws s3 cp dist/catalog/dune.html "s3://${BUCKET}/catalog/dune.html" \
  --metadata cache-tag="book:dune,category:sci-fi"
aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "#book:dune"

Derived from Invalidating content by cache tags. Tags are case-insensitive, up to 50 per object, and the header name must be configured in the distribution first. Browsers and corporate proxies keep their own copies, which an invalidation cannot touch; that is one more reason to prefer versioned file names.

Custom domains, HTTPS and Route 53

To serve www.example.com instead of the cloudfront.net name you need three things: a public certificate for the name in ACM in us-east-1, the name added to the distribution as an alternate domain name (CNAME), and a DNS record that points at the distribution. CloudFront refuses an alternate domain name that is not covered by an attached certificate, and refuses one that is already attached to a different distribution (CNAMEAlreadyExists; the owner of the apex must validate it, so a wildcard on another distribution can be used to move a name safely).

In the AWS Management Console

  1. Switch the Region selector to US East (N. Virginia) and open Certificate Manager → Request → Request a public certificate. Enter www.example.com (and example.com if needed), choose DNS validation and request it.

  2. Open the certificate and choose Create records in Route 53 (if the zone is in this account). Wait for the status Issued; ACM renews it automatically as long as the validation record stays.

  3. Open CloudFront → the distribution → General → Settings → Edit. Add www.example.com under Alternate domain names and select the certificate.

  4. In Route 53 → hosted zone → Create record, enter www, record type A, switch on Alias, choose Alias to CloudFront distribution, select the distribution, and repeat with type AAAA for IPv6.

With the AWS CLI

CERT_ARN=$(aws acm request-certificate --region us-east-1 \
  --domain-name "$DOMAIN" --validation-method DNS \
  --query CertificateArn --output text)
aws acm describe-certificate --region us-east-1 --certificate-arn "$CERT_ARN" \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord'   # create this CNAME in DNS
aws acm wait certificate-validated --region us-east-1 --certificate-arn "$CERT_ARN"

cat > alias.json <<'EOF'
{
  "Changes": [
    { "Action": "UPSERT", "ResourceRecordSet": {
        "Name": "www.example.com", "Type": "A",
        "AliasTarget": { "HostedZoneId": "Z2FDTNDATAQYW2", "DNSName": "d111111abcdef8.cloudfront.net",
                         "EvaluateTargetHealth": false } } },
    { "Action": "UPSERT", "ResourceRecordSet": {
        "Name": "www.example.com", "Type": "AAAA",
        "AliasTarget": { "HostedZoneId": "Z2FDTNDATAQYW2", "DNSName": "d111111abcdef8.cloudfront.net",
                         "EvaluateTargetHealth": false } } }
  ]
}
EOF
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch file://alias.json

Z2FDTNDATAQYW2 is the fixed hosted zone ID that identifies CloudFront in every alias record; replace the DNS name with your distribution’s. Derived from aws acm request-certificate, aws route53 change-resource-record-sets and Use custom URLs by adding alternate domain names. Certificate issuance and Route 53 in depth are on Route 53, DNS and ACM Certificates. Alias records to CloudFront are free to query and work at the zone apex, which a CNAME cannot; on a flat-rate plan they also do not count against the plan’s DNS query allowance.

HTTPS and security policies

  • Viewer protocol policy: Redirect HTTP to HTTPS for sites, HTTPS only for APIs. CloudFront does not terminate plain HTTP for anything sensitive.

  • Security policy (minimum TLS version and cipher suite for viewers): the current named policies are TLSv1.2_2021 (the long-standing recommended default), TLSv1.2_2025 and TLSv1.3_2025 (TLS 1.3 only), alongside older ones (TLSv1, TLSv1_2016, TLSv1.1_2016, TLSv1.2_2018, TLSv1.2_2019) kept for old clients. Choose the strictest your audience supports. CloudFront also offers hybrid post-quantum key exchange for TLS 1.3 connections.

  • SNI: with SNI only (the default and the only free option) clients must send the server name, which every modern client does; the legacy dedicated IP certificate option costs extra and is not allowed on flat-rate plans.

  • Origin protocol policy: use HTTPS only toward custom origins and make the origin certificate cover the name CloudFront connects to. For S3 origins with OAC the connection is always HTTPS when signing is always.

Edge compute: CloudFront Functions and Lambda@Edge

Two ways exist to run code in the request path. Prefer CloudFront Functions for lightweight header, URL and redirect logic and keep Lambda@Edge for work that needs more power.

Aspect CloudFront Functions Lambda@Edge

Runs at

Edge locations, on viewer request and viewer response only

Regional edge caches, on viewer request and response and origin request and response

Language

JavaScript (runtime 2.0, cloudfront-js-2.0, supports async/await and ES 6 to 12 features; runtime 1.0 remains)

Node.js and Python, plus the larger Lambda runtime and package size

Capabilities

Rewrite URL, headers, cookies, redirects, generate small responses, A/B routing, JWT checks with the built-in crypto module; no network or file system access, no timers; read from a KeyValueStore for dynamic data

Network calls, AWS SDK, larger logic, body access, longer execution

Scale and cost

Sub-millisecond start, per-invocation pricing, with a monthly free allowance on pay-as-you-go

Standard Lambda billing (requests and duration), created in us-east-1 and replicated

Limitation

Cannot act on origin requests or responses

Origin triggers do not work with VPC origins

CloudFront KeyValueStore is a low-latency key-value datastore that functions read at the edge, so redirect maps or feature flags change without a code deployment. Pick the smallest tool that works.

Bookshelf needs a single-page-application rewrite (unknown paths that are not files map to /index.html) and the security headers already come from the managed response headers policy. The function below replaces the custom error responses used earlier for deep links, keeping the HTTP status accurate for real missing assets:

function handler(event) {
  var request = event.request;
  var uri = request.uri;
  if (uri.endsWith('/')) {
    request.uri = uri + 'index.html';
  } else if (!uri.includes('.')) {
    request.uri = '/index.html';
  }
  return request;
}
aws cloudfront create-function --name bookshelf-spa-rewrite \
  --function-config Comment="SPA rewrite",Runtime=cloudfront-js-2.0 \
  --function-code fileb://spa-rewrite.js
aws cloudfront test-function --name bookshelf-spa-rewrite --if-match "$ETAG" \
  --stage DEVELOPMENT --event-object fileb://test-event.json
aws cloudfront publish-function --name bookshelf-spa-rewrite --if-match "$ETAG"

Associate the published function with the default behavior as a viewer-request function: in the console edit the behavior → Function associations → Viewer request → CloudFront Functions; in the distribution JSON add this to the behavior:

"FunctionAssociations": {
  "Quantity": 1,
  "Items": [{
    "EventType": "viewer-request",
    "FunctionARN": "arn:aws:cloudfront::111122223333:function/bookshelf-spa-rewrite"
  }]
}

ETAG comes from the previous command’s output (describe-function returns it too).

Private content: signed URLs and signed cookies

Public static assets need no authentication. For content that only paying or logged-in users may download (an e-book PDF in Bookshelf), mark the behavior Restrict viewer access and require signed URLs or signed cookies created with a private key; CloudFront verifies the signature with the matching public key registered in a key group. Signed URLs suit single files; signed cookies suit many files under one path. OAC still protects the S3 origin separately.

openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
KEY_ID=$(aws cloudfront create-public-key \
  --public-key-config Name=bookshelf-downloads,CallerReference=2026-10-01,EncodedKey="$(cat public_key.pem)" \
  --query 'PublicKey.Id' --output text)
aws cloudfront create-key-group --key-group-config Name=bookshelf-downloads,Items="$KEY_ID"

Add the key group’s ID to the behavior’s TrustedKeyGroups (console: Restrict viewer access → Yes → Trusted key groups) and keep the private key in Secrets Manager (see Secrets Manager, Parameter Store and KMS). Derived from Serve private content with signed URLs and signed cookies and aws cloudfront create-key-group. The Java code that signs a URL is in Application code.

Security

AWS WAF, Shield and geo restriction

  • AWS Shield Standard protects every distribution against common network and transport-layer DDoS attacks at no extra charge.

  • AWS WAF attaches a web ACL to the distribution for rate limiting, managed rule groups and bot control. A web ACL for CloudFront has global (CLOUDFRONT) scope and must be created in us-east-1. In the console, the distribution’s Security tab offers Enable security protections and creates a recommended ACL in one step. Flat-rate plans include a web ACL (and require one to stay attached).

  • Geographic restrictions allow or block countries from the distribution’s Security → Geographic restrictions tab (Restrictions.GeoRestriction in the configuration).

aws wafv2 create-web-acl --region us-east-1 --scope CLOUDFRONT \
  --name bookshelf-cdn-acl --default-action Allow={} \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=bookshelf-cdn-acl \
  --rules '[{"Name":"common","Priority":1,"OverrideAction":{"None":{}},
    "Statement":{"ManagedRuleGroupStatement":{"VendorName":"AWS","Name":"AWSManagedRulesCommonRuleSet"}},
    "VisibilityConfig":{"SampledRequestsEnabled":true,"CloudWatchMetricsEnabled":true,"MetricName":"common"}}]'

Take the returned web ACL ARN and set it as WebACLId in the distribution configuration (console: Edit the distribution’s Security settings and pick the ACL). Derived from aws wafv2 create-web-acl and Use AWS WAF protections. WAF has its own charges per web ACL, rule and request, and flat-rate plans bundle them.

IAM permissions

Two roles are typical: a pipeline role that publishes the front end and invalidates the cache, and an operator role that manages distributions. The deployment role, attached to the GitHub Actions OIDC role (see Developer Tools and DevOps and Identity and Access), needs only this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublishStaticSite",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::bookshelf-web-111122223333/*"
    },
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::bookshelf-web-111122223333"
    },
    {
      "Sid": "InvalidateCache",
      "Effect": "Allow",
      "Action": ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"],
      "Resource": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE1234"
    }
  ]
}

An operator who edits distributions and policies needs the cloudfront:* actions below (CloudFront resource-level permissions exist for distributions, policies and functions, but several list actions only accept *). The AWS managed policies CloudFrontFullAccess and CloudFrontReadOnlyAccess exist for quick starts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ManageBookshelfDistribution",
      "Effect": "Allow",
      "Action": [
        "cloudfront:GetDistribution", "cloudfront:GetDistributionConfig",
        "cloudfront:UpdateDistribution", "cloudfront:CreateInvalidation",
        "cloudfront:ListInvalidations", "cloudfront:TagResource"
      ],
      "Resource": "arn:aws:cloudfront::111122223333:distribution/E1EXAMPLE1234"
    },
    {
      "Sid": "ListAndPolicies",
      "Effect": "Allow",
      "Action": [
        "cloudfront:ListDistributions", "cloudfront:ListCachePolicies", "cloudfront:GetCachePolicy",
        "cloudfront:ListOriginRequestPolicies", "cloudfront:ListResponseHeadersPolicies",
        "cloudfront:ListOriginAccessControls", "cloudfront:GetOriginAccessControl"
      ],
      "Resource": "*"
    }
  ]
}

Derived from the Actions, resources, and condition keys for Amazon CloudFront and Identity and access management for CloudFront. Verify the exact action list against the authorization reference before relying on a trimmed policy.

VPC origins

When the origin must not be reachable from the internet at all, put the ALB, NLB or EC2 instance in a private subnet and register it as a VPC origin; CloudFront then reaches it through a service-managed network interface and the load balancer has no public IP. It needs an internet gateway on the VPC, at least one free IPv4 address in the subnet, and a security group on the origin that allows traffic either from the CloudFront managed prefix list or from the service-managed group CloudFront-VPCOrigins-Service-SG. A Network Load Balancer needs a security group and cannot have a TLS listener, Gateway Load Balancers are not supported, and Lambda@Edge origin triggers do not work with VPC origins. Create it in the console under VPC origins → Create VPC origin (status Deployed can take up to 15 minutes) or with aws cloudfront create-vpc-origin, then select it as the origin domain. VPC origins on flat-rate plans require the Business or Premium tier. See Restrict access with VPC origins and VPC Networking. For a public ALB, the classic hardening is to allow only CloudFront’s managed prefix list in the ALB security group and to require a secret header added by the distribution (OriginCustomHeaders) that the ALB listener rule checks.

Logging and monitoring

  • Standard logging (v2) delivers access logs through CloudWatch vended logs to CloudWatch Logs, Amazon Data Firehose or S3, with selectable fields and output formats (JSON, W3C, Parquet). Turn it on from the distribution’s Logging tab. The delivery APIs live in us-east-1.

  • Real-time logs stream per-request records to a Kinesis data stream within seconds, with a sampling rate; they are billed per request and are not supported on flat-rate plans.

  • Metrics (namespace AWS/CloudFront, always in us-east-1, dimension Region=Global): Requests, BytesDownloaded, BytesUploaded, 4xxErrorRate, 5xxErrorRate, TotalErrorRate. Additional metrics such as cache hit rate and origin latency can be enabled per distribution at an extra charge.

aws logs put-delivery-source --region us-east-1 --name bookshelf-cdn-src --log-type ACCESS_LOGS \
  --resource-arn "arn:aws:cloudfront::111122223333:distribution/${DIST_ID}"
aws logs put-delivery-destination --region us-east-1 --name bookshelf-cdn-dst \
  --delivery-destination-configuration destinationResourceArn=arn:aws:logs:us-east-1:111122223333:log-group:/cloudfront/bookshelf-cdn
aws logs create-delivery --region us-east-1 --delivery-source-name bookshelf-cdn-src \
  --delivery-destination-arn "arn:aws:logs:us-east-1:111122223333:delivery-destination:bookshelf-cdn-dst"

aws cloudwatch get-metric-statistics --region us-east-1 --namespace AWS/CloudFront \
  --metric-name 5xxErrorRate --dimensions Name=DistributionId,Value="$DIST_ID" Name=Region,Value=Global \
  --start-time "$(date -u -v-1H +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" --period 300 --statistics Average

(On Linux replace date -u -v-1H with date -u -d '1 hour ago'.) Derived from Configure standard logging (v2), Monitor CloudFront with CloudWatch metrics and aws logs create-delivery. Dashboards and alarms generally are on Monitoring and Observability.

Pricing model

No prices are stated here; use the CloudFront pricing page and the AWS Pricing Calculator for current figures (checked 2026-10-01). Two models coexist, chosen per distribution.

Pay-as-you-go

Billing follows the components you use. The main drivers:

  • Data transfer out to the internet, per GB and tiered by volume, with the rate depending on the viewer’s geography. The price class limits which edge locations serve you: PriceClass_100 (North America and Europe) is the cheapest, PriceClass_200 adds more Regions and PriceClass_All is global.

  • HTTP and HTTPS requests, per ten thousand, by geography and by protocol.

  • Origin traffic: data transferred from AWS origins (S3, ALB, API Gateway and so on) to CloudFront is free, which is the main reason a CDN in front of S3 usually costs less than serving from S3 directly; data to non-AWS origins is not.

  • Extras: invalidation paths beyond the monthly free 1,000, CloudFront Functions invocations, Lambda@Edge, real-time logs, Origin Shield requests, field-level encryption, dedicated-IP custom SSL, and any AWS WAF and CloudWatch Logs usage.

  • The pay-as-you-go model includes an always-free monthly allowance for data transfer, requests and function invocations; the pricing page states its current size.

Flat-rate pricing plans

In November 2025 AWS added flat-rate pricing plans: a fixed monthly price per distribution with no overage charges, bundling the CDN, AWS WAF and DDoS protection, Route 53 DNS, CloudWatch Logs ingestion, a TLS certificate, serverless edge compute and S3 storage credits. Four tiers exist (Free, Pro, Business, Premium); a higher tier includes everything below it. The published monthly allowances at the time of checking (usage allowances are not hard limits):

Free Pro Business Premium

Requests per month

1 million

10 million

125 million

500 million (configurable up to 6 billion)

Data transfer per month

100 GB

50 TB

50 TB

50 TB (configurable up to 600 TB)

Cache behaviors per distribution

5

10

50

100

S3 storage credit

5 GB

50 GB

1 TB

5 TB

Invalidation by cache tag

no

yes

yes

yes

Edge key-value store

no

yes

yes

yes

Standard access logs

no

yes

yes

yes

VPC origins and origin mutual TLS

no

no

yes

yes

Viewer mutual TLS

no

no

no

yes

Points to weigh before choosing a plan:

  • Allowances are guidance, not billing limits: an occasional spike up to three times the allowance is accommodated in the first month, sustained excess is evaluated over several months, and prolonged overuse may lead to a suggestion to upgrade or to adjusted delivery (fewer edge locations) rather than an extra bill. Traffic blocked by WAF or absorbed as DDoS does not count.

  • One plan covers one distribution and one apex domain; a plan cannot be shared. Quotas include 100 plans per account and 3 Free plans per account. An account that is using the AWS Free Tier cannot subscribe, and upgrades are prorated immediately while downgrades take effect at the next cycle.

  • Features that are not supported on a flat-rate plan and must be removed before you can subscribe: multi-tenant distributions, continuous deployment and staging distributions, the Anycast static IP list, real-time logs, the AWS WAF Targeted Bot Control, partner managed rule groups, account takeover and account creation fraud prevention rules, shared WAF rule groups, legacy ForwardedValues cache settings, dedicated-IP custom SSL, field-level encryption, IAM server certificates and origin access identity (OAI). A Web ACL must stay attached, and CloudFront Functions, key-value stores and web ACLs used by a plan cannot be shared with other distributions.

  • Everything else on this page works: OAC, cache, origin request and response headers policies, Lambda@Edge, signed URLs and cookies, and custom domains.

# Plans are managed in the console (Distribution -> General -> Manage plan) or with the pricing plan commands;
# list the CLI surface first because it is new
aws cloudfront help | grep -i -E 'plan|subscription'

See CloudFront flat-rate pricing plans for the current feature matrix and for the console and CLI procedures. A reasonable rule: use a Free plan for hobby sites and prototypes, a flat-rate tier when you want a predictable bill and the bundled WAF, and pay-as-you-go when you need a feature the plans exclude or when traffic is very low or very high. Cost control more generally is on Cost Management and Support.

Networking and scaling

  • Capacity: CloudFront scales automatically; there is nothing to size, pre-warm or provision. Your origin is the limit, which is why cache hit ratio is the main scaling lever. Origin Shield, long TTLs, a narrow cache key and request collapsing keep misses off the origin.

  • Protocols: viewers connect over HTTP/1.1, HTTP/2 and optionally HTTP/3 (QUIC), with IPv6 and WebSockets supported.

  • Timeouts: the origin response timeout (default 30 seconds, adjustable by quota) and keep-alive timeout belong to the origin configuration; a slower origin yields 504. Configure origin connection attempts and timeout and use an origin group to fail over to a secondary origin on 5xx codes.

  • Reaching the origin: allow the CloudFront managed prefix list (com.amazonaws.global.cloudfront.origin-facing) in the ALB security group instead of copying IP ranges, or use a VPC origin as described above.

  • Quotas are listed in the CloudFront quotas page: distributions per account, behaviors, origins, file size, header sizes and the invalidation limits. Request increases through the Service Quotas console; read yours with aws service-quotas list-service-quotas --service-code cloudfront --region us-east-1.

Newer distribution features

Continuous deployment lets you create a staging distribution that receives a percentage (or header-matched part) of production traffic, test a configuration change such as a new origin, and then promote it to the primary distribution; rollbacks are a promotion in the other direction. Multi-tenant distributions (CloudFront SaaS Manager) let one distribution definition serve many customer domains through distribution tenants with their own certificates and parameters, which is useful for white-label SaaS and not needed for Bookshelf. Neither is supported on flat-rate plans. See Use continuous deployment.

Application code

Java application code uses the AWS SDK for Java 2.x with the default credential chain (a task role, Lambda role or Pod Identity role; never access keys). Two typical uses are a deployment step that invalidates index.html, and the back end signing a download URL for a book PDF served from a private path:

// build.gradle / pom.xml: software.amazon.awssdk:cloudfront
import software.amazon.awssdk.services.cloudfront.CloudFrontClient;
import software.amazon.awssdk.services.cloudfront.CloudFrontUtilities;
import software.amazon.awssdk.services.cloudfront.model.CannedSignerRequest;

import java.nio.file.Path;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class BookshelfCdn {

    public void invalidateIndex(String distributionId) {
        try (CloudFrontClient cloudFront = CloudFrontClient.create()) {
            cloudFront.createInvalidation(r -> r
                .distributionId(distributionId)
                .invalidationBatch(b -> b
                    .callerReference(Long.toString(System.currentTimeMillis()))
                    .paths(p -> p.quantity(1).items("/index.html"))));
        }
    }

    public String signedDownloadUrl(String keyPairId, Path privateKeyPkcs8Pem) throws Exception {
        CannedSignerRequest request = CannedSignerRequest.builder()
            .resourceUrl("https://www.example.com/private/dune.pdf")
            .privateKey(privateKeyPkcs8Pem)
            .keyPairId(keyPairId)                       // the public key ID in the key group
            .expirationDate(Instant.now().plus(15, ChronoUnit.MINUTES))
            .build();
        return CloudFrontUtilities.create().getSignedUrlWithCannedPolicy(request).url();
    }
}

Derived from the AWS SDK for Java 2.x CloudFront examples and the CloudFront client API reference. The private key must be in PKCS#8 PEM format (openssl pkcs8 -topk8 -nocrypt -in private_key.pem -out private_key_pkcs8.pem). Spring Boot services that read from S3 directly are covered on File Storage & Object Stores.

Troubleshooting

Symptom Likely cause Check and fix

403 AccessDenied from CloudFront for objects that exist in a private S3 bucket

The bucket policy does not allow cloudfront.amazonaws.com, the AWS:SourceArn is a different distribution ID or account, the origin has no OAC attached, or the OAC signing is never.

aws s3api get-bucket-policy, compare the ARN with aws cloudfront get-distribution, edit the origin and select the OAC, set signing to always. For SSE-KMS add the key policy statement.

403 for a key that does not exist (not 404)

S3 hides absence without s3:ListBucket.

Grant s3:ListBucket to the distribution on the bucket ARN or map 403 and 404 to index.html for an SPA.

403 or 404 on /about after a page refresh in the SPA

The path is not an object; CloudFront asks S3 for the key about.

Add the viewer-request rewrite function or custom error responses shown above.

Website endpoint origin works with public objects but fails with OAC

OAC does not apply to S3 website endpoints.

Use the REST endpoint (<bucket>.s3.<region>.amazonaws.com) as the origin.

502 Bad Gateway

CloudFront cannot negotiate TLS with a custom origin: certificate hostname mismatch, expired or self-signed certificate, or unsupported protocols.

Make the origin certificate cover the origin domain name configured in CloudFront; use HTTPS only with a valid ACM or public certificate.

504 Gateway Timeout

The origin did not answer within the origin response timeout or refused CloudFront connections (security group, VPC origin not deployed).

Allow the CloudFront prefix list on the origin, raise the origin timeout within quota, fix slow endpoints.

Old content after a release

Cached copy still valid; the browser caches index.html too.

Invalidate /index.html, use hashed asset names and Cache-Control: no-cache on index.html.

Every request is a Miss from cloudfront

The cache key includes a high-cardinality value (all cookies or query strings), the origin sends Cache-Control: private or no-store, or the behavior uses CachingDisabled.

Compare the response headers, move forwarded values from the cache policy to the origin request policy, use UseOriginCacheControlHeaders deliberately.

InvalidViewerCertificate or the certificate is not selectable

The ACM certificate is not in us-east-1, is not yet Issued, or does not cover the alternate domain name.

Request or import the certificate in N. Virginia and wait for validation.

CNAMEAlreadyExists when adding an alternate domain name

The name is attached to another distribution (possibly in another account).

Remove it there, or prove ownership via a wildcard certificate so the name can be moved.

API works directly but fails through the /api/* behavior

Methods other than GET and HEAD not allowed, or the wrong Host header forwarded (API Gateway and function URLs need their own), or auth headers dropped.

Allow all methods, use AllViewerExceptHostHeader for API Gateway, forward Authorization through the origin request policy.

CORS errors in the browser

The cache stores a response without CORS headers, or Origin is not forwarded.

Use CORS-S3Origin for S3 and add a response headers policy; make the cache key include Origin if responses vary.

Cannot delete a distribution

It is still enabled, or an OAC, function or pricing plan association remains.

Disable it, wait for Deployed, then delete (see Clean up).

Useful commands: aws cloudfront get-distribution --id "$DIST_ID" (status Deployed or InProgress), aws cloudfront list-invalidations --distribution-id "$DIST_ID", curl -sI to read X-Cache and X-Amz-Cf-Id, and the CloudFront Monitoring tab. The official CloudFront troubleshooting chapter lists every HTTP status.

Where older guides are outdated

  • Public bucket plus website hosting was the classic static-site recipe. New buckets have Block Public Access on and ACLs disabled by default, so that approach fails; use a private bucket with OAC and CloudFront.

  • Origin access identity appears in many tutorials and in older CloudFormation templates. Use OAC, which also works with SSE-KMS, PUT and DELETE and every Region.

  • ForwardedValues and per-behavior TTLs are replaced by cache, origin request and response headers policies.

  • Dedicated IP custom SSL is no longer needed; use SNI with ACM certificates.

  • CloudFront Functions runtime 1.0 has fewer language features than runtime 2.0.

  • Pay-as-you-go only is no longer the only model; flat-rate plans are new since November 2025.

  • Standard logs to S3 (legacy) still work, but standard logging v2 is the current option.

Clean up

A distribution cannot be deleted while it is enabled, and an S3 bucket policy that names a deleted distribution is harmless but should be removed. Delete in this order:

# 1. Disable the distribution (read the config and ETag, flip Enabled to false, push it back)
aws cloudfront get-distribution-config --id "$DIST_ID" > dist.json
ETAG=$(jq -r .ETag dist.json)
jq '.DistributionConfig | .Enabled = false' dist.json > dist-disabled.json
aws cloudfront update-distribution --id "$DIST_ID" --if-match "$ETAG" --distribution-config file://dist-disabled.json
aws cloudfront wait distribution-deployed --id "$DIST_ID"

# 2. Delete it with the new ETag
ETAG=$(aws cloudfront get-distribution --id "$DIST_ID" --query ETag --output text)
aws cloudfront delete-distribution --id "$DIST_ID" --if-match "$ETAG"

# 3. Delete the OAC, functions, key group and public key, and policies you created
aws cloudfront delete-origin-access-control --id "$OAC_ID" \
  --if-match "$(aws cloudfront get-origin-access-control --id "$OAC_ID" --query ETag --output text)"
aws cloudfront delete-function --name bookshelf-spa-rewrite \
  --if-match "$(aws cloudfront describe-function --name bookshelf-spa-rewrite --query ETag --output text)"

# 4. DNS, certificate, WAF, bucket policy
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE_ID" --change-batch file://alias-delete.json
aws acm delete-certificate --region us-east-1 --certificate-arn "$CERT_ARN"
aws wafv2 delete-web-acl --region us-east-1 --scope CLOUDFRONT --name bookshelf-cdn-acl --id "$ACL_ID" --lock-token "$LOCK_TOKEN"
aws s3api delete-bucket-policy --bucket "$BUCKET"

If you deployed with CloudFormation, aws cloudformation delete-stack --stack-name bookshelf-cdn removes the distribution, OAC and bucket policy together (CloudFormation disables the distribution first, which takes several minutes); with CDK use cdk destroy. alias-delete.json is the change batch from earlier with "Action": "DELETE". Delete the key group and public key with delete-key-group and delete-public-key, and any custom cache, origin request and response headers policies you created (delete-cache-policy, and so on) once no behavior uses them. Derived from aws cloudfront delete-distribution and Delete a distribution. In the console: select the distribution → Disable, wait for the status to settle, then Delete; remove the OAC under Security → Origin access. Keep the bucket if it holds the site you still want.