CloudFront Cache Invalidation: Force-Refresh Stale Edge Content After S3 Updates

You've uploaded a new version of a file to S3, but CloudFront's edge locations are still serving the old cached copy — a frustrating gap between your deployment and what users actually see. This post explains exactly why this happens and how to use CloudFront Invalidations to purge stale content from the edge cache immediately.

TL;DR

StepActionEffect
1Upload new file to S3Origin has new content; edge caches still hold old TTL-valid copy
2Create CloudFront InvalidationMarks cached objects at all edge locations as expired
3Next request hits edgeCache miss → edge fetches fresh copy from S3 origin
4Subsequent requestsServed from refreshed edge cache at full CDN speed

Why CloudFront Doesn't Automatically Detect S3 Changes

CloudFront is a pull-through CDN. When an edge location receives a request for an object, it fetches it from the origin (S3), caches it locally, and serves subsequent requests from that local cache until the object's TTL (Time-To-Live) expires. CloudFront does not poll S3 for changes — it has no event-driven awareness of object updates. The edge cache is authoritative until the TTL elapses or you explicitly invalidate the object.

TTL is controlled by the Cache-Control header on the S3 object (e.g., max-age=86400 means 24 hours) or by the CloudFront distribution's default TTL settings. A file replaced in S3 with the same key will sit invisible behind a valid cache entry for the entire remaining TTL duration.

Request Flow: Cached vs. Invalidated

sequenceDiagram participant User as "User Browser" participant Edge as "CloudFront Edge (PoP)" participant S3 as "S3 Origin" participant Dev as "Developer / CI-CD" User->>Edge: GET /assets/app.js Edge-->>User: "Cache HIT — returns OLD version (TTL valid)" Note over Dev: Uploads new app.js to S3 Dev->>S3: PutObject /assets/app.js (new version) User->>Edge: GET /assets/app.js Edge-->>User: "Cache HIT — STILL returns OLD version" Dev->>Edge: CreateInvalidation /assets/app.js Note over Edge: Marks cached object as EXPIRED
across all edge locations User->>Edge: GET /assets/app.js Edge->>S3: "Cache MISS — fetches from origin" S3-->>Edge: Returns NEW version of app.js Edge-->>User: "Serves NEW version + repopulates cache"
  1. User Request: The browser requests /assets/app.js from the CloudFront distribution domain.
  2. Edge Cache Check: The nearest edge location (Point of Presence) checks its local cache.
  3. Cache HIT (stale path): If the object is cached and TTL has not expired, the old version is returned directly — S3 is never contacted.
  4. Invalidation Issued: You create an invalidation for /assets/app.js. CloudFront propagates this to all edge locations, marking the cached copy as expired.
  5. Cache MISS (post-invalidation): The next request finds no valid cache entry; the edge fetches the object from S3 origin.
  6. Cache Repopulated: The fresh object is stored at the edge and served to the user.
Analogy: Think of CloudFront edge caches like a chain of local convenience stores stocked from a central warehouse (S3). Updating the warehouse doesn't automatically restock the stores — the shelves hold their current inventory until the restock schedule (TTL) triggers. An invalidation is like calling every store manager simultaneously and saying: "Throw out what's on the shelf right now and reorder from the warehouse on the next customer request."

Creating an Invalidation: Three Methods

Method 1: AWS Management Console

  1. Open the CloudFront console → select your distribution.
  2. Navigate to the Invalidations tab → click Create invalidation.
  3. Enter the object path(s). Use /assets/app.js for a single file or /assets/* for a path wildcard.
  4. Click Create invalidation. Status will show In Progress then Completed.

Method 2: AWS CLI (Recommended for Automation)

The following command creates an invalidation for a single file. Replace EDFDVBD6EXAMPLE with your actual distribution ID.

aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/assets/app.js"

To invalidate multiple specific paths:

aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/assets/app.js" "/assets/style.css" "/index.html"

To invalidate all objects in the distribution (use sparingly — see cost note below):

aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/*"

Method 3: AWS SDK (Python/Boto3) for CI/CD Integration

🔽 [Click to expand] — Python Boto3 Invalidation Script
import boto3
import time

def create_cloudfront_invalidation(distribution_id: str, paths: list[str]) -> dict:
    """
    Creates a CloudFront invalidation and waits for completion.

    Args:
        distribution_id: The CloudFront distribution ID (e.g., 'EDFDVBD6EXAMPLE')
        paths: List of object paths to invalidate (e.g., ['/assets/app.js'])

    Returns:
        The invalidation response dict from AWS.
    """
    client = boto3.client('cloudfront')

    caller_reference = str(int(time.time()))  # Unique string per request

    response = client.create_invalidation(
        DistributionId=distribution_id,
        InvalidationBatch={
            'Paths': {
                'Quantity': len(paths),
                'Items': paths
            },
            'CallerReference': caller_reference
        }
    )

    invalidation_id = response['Invalidation']['Id']
    print(f"Invalidation created: {invalidation_id}")
    print(f"Status: {response['Invalidation']['Status']}")

    # Optional: wait for completion
    waiter = client.get_waiter('invalidation_completed')
    print("Waiting for invalidation to complete...")
    waiter.wait(
        DistributionId=distribution_id,
        Id=invalidation_id
    )
    print("Invalidation completed.")

    return response


if __name__ == "__main__":
    create_cloudfront_invalidation(
        distribution_id="EDFDVBD6EXAMPLE",
        paths=["/assets/app.js", "/index.html"]
    )

IAM Permissions Required

Apply least-privilege: the identity creating invalidations needs only the cloudfront:CreateInvalidation action scoped to the specific distribution ARN.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudfrontInvalidation",
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
    }
  ]
}

Note: CloudFront is a global service; the ARN format omits the region segment, as shown above.

Invalidation Propagation Flow

graph TD Dev["Developer / CI-CD Pipeline"] -->|"aws cloudfront create-invalidation"| API["CloudFront Control Plane API"] API --> CP["CloudFront Control Plane
Assigns Invalidation ID
Status: InProgress"] CP --> E1["Edge Location
us-east-1"] CP --> E2["Edge Location
eu-west-1"] CP --> E3["Edge Location
ap-southeast-1"] E1 -->|"Purges cached object"| P1["Cache Expired"] E2 -->|"Purges cached object"| P2["Cache Expired"] E3 -->|"Purges cached object"| P3["Cache Expired"] P1 & P2 & P3 --> Done["Status: Completed"] Done --> NextReq["Next User Request
→ Cache MISS
→ Fetch from S3 Origin"]
  1. API Call: Your CLI/SDK/Console call hits the CloudFront control plane API.
  2. Control Plane: CloudFront registers the invalidation and assigns it an ID and InProgress status.
  3. Edge Propagation: The invalidation signal is distributed to all edge locations (Points of Presence) globally that hold a cached copy of the specified path(s).
  4. Cache Purge: Each edge location marks the matching cached objects as expired.
  5. Status Update: Once all edges confirm, the invalidation status transitions to Completed.
  6. Origin Fetch: The next user request to any edge triggers a fresh fetch from S3, repopulating the cache with the new file version.

Cost & Best Practice Considerations

ConsiderationDetail
Free tierThe first 1,000 invalidation paths per month are free. Beyond that, charges apply per path. Check the official CloudFront pricing page for current rates.
Wildcard cost/* counts as one path for billing, but invalidates all cached objects — use it deliberately.
Preferred alternativeUse versioned file names (e.g., app.v2.js) or content-hash filenames (e.g., app.3f8a2c.js) in your build pipeline. This makes every deployment a cache miss by design, eliminating the need for invalidations entirely.
Propagation timeInvalidations typically complete within a few minutes but are not instantaneous. Propagation time varies; do not assume sub-second completion.

Monitoring Invalidation Status via CLI

# List recent invalidations for a distribution
aws cloudfront list-invalidations \
  --distribution-id EDFDVBD6EXAMPLE

# Get status of a specific invalidation
aws cloudfront get-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --id I2J3K4L5M6EXAMPLE

Glossary

TermDefinition
Edge Location (PoP)A CloudFront Point of Presence — a geographically distributed server that caches and serves content close to end users.
TTL (Time-To-Live)The duration an object remains valid in the edge cache before CloudFront re-validates it with the origin.
InvalidationA CloudFront API operation that forces cached objects matching specified paths to be treated as expired across all edge locations.
CallerReferenceA unique string you provide per invalidation API call to ensure idempotency and prevent duplicate submissions.
Cache-Control HeaderAn HTTP header set on S3 objects that instructs CloudFront (and browsers) how long to cache the object before re-fetching.

Wrap-Up & Next Steps

CloudFront invalidations are the correct tool for immediate cache purging after an S3 update, but they are a reactive fix. For production deployments, adopt a versioned/hashed filename strategy in your CI/CD pipeline to make cache invalidation unnecessary by default — every new file gets a new cache key automatically.

Comments

Popular posts from this blog

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

Lambda Infinite Loop with S3: How to Prevent Recursive Triggers