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
| Step | Action | Effect |
|---|---|---|
| 1 | Upload new file to S3 | Origin has new content; edge caches still hold old TTL-valid copy |
| 2 | Create CloudFront Invalidation | Marks cached objects at all edge locations as expired |
| 3 | Next request hits edge | Cache miss → edge fetches fresh copy from S3 origin |
| 4 | Subsequent requests | Served 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
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"
- User Request: The browser requests
/assets/app.jsfrom the CloudFront distribution domain. - Edge Cache Check: The nearest edge location (Point of Presence) checks its local cache.
- Cache HIT (stale path): If the object is cached and TTL has not expired, the old version is returned directly — S3 is never contacted.
- Invalidation Issued: You create an invalidation for
/assets/app.js. CloudFront propagates this to all edge locations, marking the cached copy as expired. - Cache MISS (post-invalidation): The next request finds no valid cache entry; the edge fetches the object from S3 origin.
- 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
- Open the CloudFront console → select your distribution.
- Navigate to the Invalidations tab → click Create invalidation.
- Enter the object path(s). Use
/assets/app.jsfor a single file or/assets/*for a path wildcard. - Click Create invalidation. Status will show
In ProgressthenCompleted.
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
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"]
- API Call: Your CLI/SDK/Console call hits the CloudFront control plane API.
- Control Plane: CloudFront registers the invalidation and assigns it an ID and
InProgressstatus. - 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).
- Cache Purge: Each edge location marks the matching cached objects as expired.
- Status Update: Once all edges confirm, the invalidation status transitions to
Completed. - 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
| Consideration | Detail |
|---|---|
| Free tier | The 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 alternative | Use 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 time | Invalidations 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
| Term | Definition |
|---|---|
| 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. |
| Invalidation | A CloudFront API operation that forces cached objects matching specified paths to be treated as expired across all edge locations. |
| CallerReference | A unique string you provide per invalidation API call to ensure idempotency and prevent duplicate submissions. |
| Cache-Control Header | An 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
Post a Comment