A client’s marketing site went “down” the morning after a content refresh — except it wasn’t down, it was serving yesterday’s homepage. The team had run a cache invalidation, watched it complete in the console, and still got the old page from three different browsers. The real problem: they’d invalidated /index.html but the object requests were hitting /, a different cache key entirely, and a second distribution behind the same DNS record hadn’t been touched at all. Two separate issues stacked on top of each other, both looking like “CloudFront is broken.”

CloudFront failures are rarely CloudFront’s fault — they’re origin access, cache key, or origin health problems wearing a CDN costume. Here’s how to pull them apart.

The Problem

Requests through a CloudFront distribution fail or return unexpected content:

Symptom What It Means
403 Forbidden from CloudFront, works direct to origin Origin Access Control/Identity misconfigured, or bucket policy doesn’t trust CloudFront
504 Gateway Timeout Origin took too long to respond, or origin is unreachable from CloudFront’s network
Invalidation completes but content is still stale Wrong path invalidated, or a downstream/browser cache is serving the old copy
Different behavior per edge location Origin failover misconfigured, or regional origin outage
403 only on some paths Behaviors/path patterns pointing at the wrong origin or cache policy

CloudFront sits between the user and the truth, so the error you see is CloudFront’s, but the cause is almost always one layer behind it.

Why Does This Happen?

  • S3 bucket policy doesn’t trust the distribution: With Origin Access Control (OAC), the bucket policy must explicitly allow the CloudFront service principal scoped to your distribution’s ARN. Forgetting this — or leaving an old Origin Access Identity policy in place after migrating to OAC — produces a clean 403 on every request.
  • Origin timeout too aggressive or too generous: The default origin response timeout is 30 seconds. A slow Lambda-backed API or cold ALB target can blow past that and CloudFront returns 504 even though the origin would have eventually responded.
  • Invalidating the wrong cache key: CloudFront caches by the full request key, which can include the query string and headers depending on your cache policy. Invalidating /index.html doesn’t touch / if that’s a distinct object, and vice versa.
  • Browser or intermediate proxy caching independently: Cache-Control headers set by the origin control browser caching separately from CloudFront’s edge cache. An invalidation clears CloudFront’s copy but not what’s already sitting in the visitor’s browser.
  • Origin group failover misrouting: If you configured primary/secondary origin failover, a permanent 403 from the primary (like a bad bucket policy) can trigger failover to a secondary that serves entirely different content — confusing everyone about which origin is “live.”
  • Security group or NACL blocking CloudFront’s IP ranges: For ALB or custom HTTP origins, if the origin’s security group doesn’t allow the CloudFront managed prefix list, every request looks like a timeout from CloudFront’s side.

The Fix

Step 1: Reproduce Against the Origin Directly

Isolate CloudFront from the equation first:

curl -I https://my-bucket.s3.amazonaws.com/index.html
curl -I http://my-internal-alb-1234567890.us-east-1.elb.amazonaws.com/

If the origin itself returns an error, fix that first — CloudFront is just relaying it.

Step 2: Check the Bucket Policy Against the Distribution

aws s3api get-bucket-policy \
  --bucket my-bucket \
  --query "Policy" \
  --output text

Confirm it includes a statement scoped to your OAC-enabled distribution:

{
  "Effect": "Allow",
  "Principal": { "Service": "cloudfront.amazonaws.com" },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-bucket/*",
  "Condition": {
    "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1A2B3C4D5E6F7"
    }
  }
}

If it’s still referencing an Origin Access Identity ARN after an OAC migration, that’s your 403.

Step 3: Inspect Origin Configuration and Timeouts

aws cloudfront get-distribution-config \
  --id E1A2B3C4D5E6F7 \
  --query "DistributionConfig.Origins.Items[*].{Domain:DomainName,ConnectTimeout:ConnectionTimeout,ReadTimeout:OriginReadTimeout}" \
  --output table

If the origin is genuinely slow, raise OriginReadTimeout (up to 60 seconds) rather than assuming CloudFront is misbehaving:

aws cloudfront update-distribution \
  --id E1A2B3C4D5E6F7 \
  --distribution-config file://updated-config.json

Step 4: Confirm What Cache Key an Invalidation Actually Targets

aws cloudfront list-invalidations \
  --distribution-id E1A2B3C4D5E6F7 \
  --query "InvalidationList.Items[*].{Id:Id,Status:Status}" \
  --output table
aws cloudfront get-invalidation \
  --distribution-id E1A2B3C4D5E6F7 \
  --id I1A2B3C4D5E6F7 \
  --query "Invalidation.InvalidationBatch.Paths.Items" \
  --output text

Make sure the path list matches the actual request path, including trailing slash and default root object behavior. When in doubt, invalidate both /index.html and /:

aws cloudfront create-invalidation \
  --distribution-id E1A2B3C4D5E6F7 \
  --paths "/index.html" "/" "/*"

Step 5: Bypass the Browser Cache to Isolate CloudFront’s Copy

curl -sI "https://www.example.com/index.html?cache-bust=$(date +%s)" | grep -i x-cache

X-Cache: Miss from cloudfront followed by a correct response confirms CloudFront’s edge cache is clean and any remaining staleness is downstream (browser or corporate proxy).

Step 6: Check Origin Group Failover Status

aws cloudfront get-distribution-config \
  --id E1A2B3C4D5E6F7 \
  --query "DistributionConfig.OriginGroups.Items[*].FailoverCriteria.StatusCodes.Items" \
  --output json

If 403 is listed as a failover trigger and your primary origin is misconfigured (not actually down), you’re being served by the secondary without realizing it. Fix the primary’s access policy rather than tuning failover further.

Step 7: Verify Security Group Rules for Custom Origins

aws ec2 describe-security-groups \
  --group-ids sg-0abc123def \
  --query "SecurityGroups[0].IpPermissions[?FromPort==\`443\`]"

Ensure the CloudFront managed prefix list is allowed:

aws ec2 describe-managed-prefix-lists \
  --query "PrefixLists[?PrefixListName=='com.amazonaws.global.cloudfront.origin-facing'].PrefixListId" \
  --output text

Is This Safe?

Yes, mostly. All the get and describe commands are read-only. Creating an invalidation is safe but not free — the first 1,000 paths per month are included, after that you’re billed per path, so avoid invalidating /* habitually as a reflex fix. Updating the bucket policy or distribution config takes effect within minutes for the policy and up to 15 minutes for distribution-wide config changes as they propagate to edge locations — test in a maintenance window if the site is high-traffic.

Key Takeaway

When CloudFront returns an error, work backward through the request path: origin directly, then origin access policy, then cache behavior, then the edge cache itself. Most “CloudFront is broken” tickets are actually an S3 bucket policy that wasn’t updated for OAC, an origin that’s genuinely too slow, or an invalidation that targeted the wrong cache key. CloudFront is doing exactly what it was configured to do — the fix is almost never in CloudFront itself.


Have questions or ran into a different CloudFront issue? Connect with me on LinkedIn or X.