AWS CloudFront Invalidation — The Complete Guide
CloudFront caches your assets at edge locations worldwide, but when a file changes you need a reliable way to evict stale copies before the TTL expires — and doing that wrong costs money, time, or both.
When to Use CloudFront Invalidation
CloudFront invalidation is the right tool when you are already serving assets through a CloudFront distribution backed by an S3 origin and you need to force edge nodes to re-fetch specific paths immediately. Before reaching for invalidation, compare it to the alternatives covered across CDN purge strategies:
| Scenario | Best tool |
|---|---|
| Assets served through CloudFront + S3 | CloudFront create-invalidation (this guide) |
| Assets served through Cloudflare | Cloudflare cache rules and purge |
| Assets served through Fastly | Fastly instant purge |
| Self-hosted Nginx reverse proxy | Nginx cache purge for fingerprinted assets |
| Fingerprinted filenames in production | Zero invalidations needed — see content hashing |
The strongest advice in this guide: fingerprint your assets with a content hash and you almost never need to invalidate. When the filename changes, CloudFront treats it as a new object. You get instant cache coherence at zero cost and zero operational risk. Invalidation is a fallback for files you genuinely cannot rename — /index.html, /robots.txt, /manifest.json — or for emergency rollbacks before a deployment pipeline can cut a new hash.
When you do need invalidation, CloudFront is faster than many people expect. In practice, propagation across all edge POPs typically completes in under 60 seconds for most regions, though Amazon’s SLA is up to 10 minutes.
Prerequisites
- AWS CLI v2.x —
aws --versionshould printaws-cli/2.*. Version 1 works but flag names for wait operations differ slightly. - Python 3.9+ and boto3 1.26+ for the programmatic examples (
pip install boto3). - An existing CloudFront distribution. Export its ID once:
export DIST_ID=E1ABCDEF2GHIJK— every snippet below uses this variable. - IAM permissions:
cloudfront:CreateInvalidation,cloudfront:GetInvalidation,cloudfront:ListInvalidationson the distribution ARN, pluss3:GetObject/s3:ListBucketon the S3 bucket if you are configuring a new origin. - For S3 origins: Origin Access Control (OAC) rather than the legacy Origin Access Identity (OAI). OAC is the current AWS recommendation and supports SSE-KMS buckets.
Configuration Reference
These are the CloudFront cache-behavior fields most relevant to invalidation strategy. They appear in distribution configs as JSON (under CacheBehaviors[*] or DefaultCacheBehavior) and in the AWS console under the Cache behavior tab.
| Field | Type | Default | Effect |
|---|---|---|---|
MinTTL |
integer (seconds) | 0 | Minimum time CloudFront caches an object. When set to 0, CloudFront honors Cache-Control: max-age and Expires headers from the origin. When > 0, CloudFront ignores those headers and caches for at least MinTTL seconds — invalidation becomes your only escape hatch. |
DefaultTTL |
integer (seconds) | 86400 | Time CloudFront caches objects that have no Cache-Control or Expires header. Irrelevant when the origin always sends Cache-Control. |
MaxTTL |
integer (seconds) | 31536000 | Caps how long CloudFront caches regardless of origin headers. An origin sending Cache-Control: max-age=99999999 is clamped to this value. |
Compress |
boolean | false | Enables automatic gzip/Brotli compression at the edge. No cache-coherence impact but affects the object key when Accept-Encoding is in the cache policy. |
QueryStringCaching |
string | none |
Controls whether query strings are included in the cache key. Relevant if you use ?v=<hash> versioning instead of filename hashing — covered in cache key architecture. |
OriginProtocolPolicy |
string | https-only |
Protocol CloudFront uses to talk to the origin. S3 REST API endpoints require https-only. |
ViewerProtocolPolicy |
string | redirect-to-https |
Whether viewers can use HTTP. Does not affect caching but affects security posture. |
The most critical interaction: MinTTL=0 is the configuration that allows fingerprinted assets to work harmoniously with Cache-Control: max-age=31536000, immutable. Your HTML files should use a DefaultTTL of 0 or very low (60 seconds), while fingerprinted JS/CSS/image files use max-age=31536000. See Cache-Control: immutable for the full header strategy, and Cache-Control immutable and TTL tuning for how to set TTLs per path pattern in CloudFront.
Step-by-Step Implementation
1. Export the distribution ID
Every command in this guide uses the DIST_ID environment variable. Set it once per shell session:
export DIST_ID=E1ABCDEF2GHIJK
Find your distribution ID in the AWS console under CloudFront → Distributions, or via:
aws cloudfront list-distributions \
--query "DistributionList.Items[*].{ID:Id,Domain:DomainName,Status:Status}" \
--output table
2. Create a simple invalidation via AWS CLI
Single path — the most targeted and cheapest form:
aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/index.html"
Wildcard for an asset directory — invalidates every object whose path starts with /assets/. Still counts as one invalidation path toward the monthly free tier:
aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/assets/*"
Full distribution sweep — use sparingly. /\* counts as one path but triggers re-fetching every cached object in the distribution. On large distributions this increases origin load significantly:
aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/*"
Multiple specific paths in one API call (counts as N paths, not 1):
aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/index.html" "/manifest.json" "/robots.txt" "/sw.js"
The CLI prints the invalidation ID and its InProgress status. CloudFront processes up to 15 invalidations concurrently per distribution. If you submit a 16th while 15 are running, the API returns a TooManyInvalidationsInProgress error.
3. Bash script with wait-for-completion
Deployments often need to block until invalidation finishes before updating DNS or running smoke tests. This script submits an invalidation and polls until Completed:
#!/usr/bin/env bash
set -euo pipefail
: "${DIST_ID:?DIST_ID must be set}"
PATHS=("$@")
if [[ ${#PATHS[@]} -eq 0 ]]; then
echo "Usage: $0 /path1 /path2 ..." >&2
exit 1
fi
echo "Creating CloudFront invalidation on $DIST_ID for: ${PATHS[*]}"
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "${PATHS[@]}" \
--query "Invalidation.Id" \
--output text)
echo "Invalidation ID: $INVALIDATION_ID"
echo "Waiting for completion (polling every 10 s)..."
while true; do
STATUS=$(aws cloudfront get-invalidation \
--distribution-id "$DIST_ID" \
--id "$INVALIDATION_ID" \
--query "Invalidation.Status" \
--output text)
echo " Status: $STATUS"
if [[ "$STATUS" == "Completed" ]]; then
echo "Invalidation complete."
break
fi
sleep 10
done
Save as invalidate.sh, make it executable (chmod +x invalidate.sh), then call it from a CI step:
./invalidate.sh /index.html /manifest.json /sw.js
The AWS CLI also exposes aws cloudfront wait invalidation-completed which uses exponential backoff internally, but it times out after 10 minutes and can be opaque in CI logs. The explicit loop above gives you line-by-line status output.
4. boto3 Python script for programmatic invalidation
Use this in Lambda functions, Python-based deploy scripts, or anywhere you need programmatic control over invalidation batching:
#!/usr/bin/env python3
"""
cloudfront_invalidate.py — programmatic CloudFront invalidation via boto3.
Usage: python cloudfront_invalidate.py E1ABCDEF2GHIJK /index.html /manifest.json
"""
import sys
import time
import uuid
import boto3
from botocore.exceptions import ClientError
def create_invalidation(dist_id: str, paths: list[str]) -> str:
"""Submit a CloudFront invalidation and return the invalidation ID."""
client = boto3.client("cloudfront")
caller_ref = str(uuid.uuid4()) # Must be unique per request
try:
response = client.create_invalidation(
DistributionId=dist_id,
InvalidationBatch={
"Paths": {
"Quantity": len(paths),
"Items": paths,
},
"CallerReference": caller_ref,
},
)
except ClientError as exc:
code = exc.response["Error"]["Code"]
if code == "TooManyInvalidationsInProgress":
raise RuntimeError(
"15 concurrent invalidations already in progress. "
"Wait for existing ones to complete or batch your paths."
) from exc
raise
inv_id = response["Invalidation"]["Id"]
print(f"Created invalidation {inv_id} (CallerReference: {caller_ref})")
return inv_id
def wait_for_completion(dist_id: str, inv_id: str, poll_interval: int = 10) -> None:
"""Poll until the invalidation status is Completed."""
client = boto3.client("cloudfront")
print(f"Waiting for invalidation {inv_id} to complete...")
while True:
response = client.get_invalidation(
DistributionId=dist_id,
Id=inv_id,
)
status = response["Invalidation"]["Status"]
print(f" Status: {status}")
if status == "Completed":
print("Invalidation complete.")
return
time.sleep(poll_interval)
def main() -> None:
if len(sys.argv) < 3:
print(f"Usage: {sys.argv[0]} <DIST_ID> <path1> [path2 ...]", file=sys.stderr)
sys.exit(1)
dist_id = sys.argv[1]
paths = sys.argv[2:]
inv_id = create_invalidation(dist_id, paths)
wait_for_completion(dist_id, inv_id)
if __name__ == "__main__":
main()
Run it:
python cloudfront_invalidate.py "$DIST_ID" /index.html /manifest.json
The CallerReference field must be unique per invalidation request. Using uuid.uuid4() is the standard pattern. If you retry the same CallerReference within 24 hours, CloudFront returns the original invalidation rather than creating a new one — useful for idempotent retries.
5. S3 origin with OAC — bucket policy
OAC (Origin Access Control) is the current recommended way to lock an S3 bucket to CloudFront-only access. The bucket must have Block Public Access enabled. The bucket policy grants read access only to your specific CloudFront distribution:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOACReadAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT-ID:distribution/E1ABCDEF2GHIJK"
}
}
}
]
}
Replace YOUR-BUCKET-NAME, ACCOUNT-ID, and E1ABCDEF2GHIJK with real values. Apply it:
aws s3api put-bucket-policy \
--bucket YOUR-BUCKET-NAME \
--policy file://bucket-policy.json
The OAC configuration on the CloudFront side references an OAC resource ID, which you create once per region:
OAC_ID=$(aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "my-site-oac",
"Description": "OAC for static site assets",
"SigningProtocol": "sigv4",
"SigningBehavior": "always",
"OriginAccessControlOriginType": "s3"
}' \
--query "OriginAccessControl.Id" \
--output text)
echo "OAC ID: $OAC_ID"
Then attach it to your distribution’s origin config via aws cloudfront update-distribution or in your infrastructure-as-code template.
6. Cache behavior TTL configuration
Apply per-path TTL policies by defining multiple cache behaviors. This JSON fragment is the shape used in aws cloudfront update-distribution --distribution-config:
{
"CacheBehaviors": {
"Quantity": 2,
"Items": [
{
"PathPattern": "/assets/*",
"TargetOriginId": "S3-YOUR-BUCKET-NAME",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true,
"MinTTL": 0,
"DefaultTTL": 31536000,
"MaxTTL": 31536000
},
{
"PathPattern": "/index.html",
"TargetOriginId": "S3-YOUR-BUCKET-NAME",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true,
"MinTTL": 0,
"DefaultTTL": 60,
"MaxTTL": 300
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "S3-YOUR-BUCKET-NAME",
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true,
"MinTTL": 0,
"DefaultTTL": 86400,
"MaxTTL": 31536000
}
}
CachePolicyId 658327ea-f89d-4fab-a63d-7e88639e58f6 is the AWS-managed CachingOptimized policy. Substitute your own policy ID if you have custom query-string or header requirements.
The two-behavior setup above gives fingerprinted assets under /assets/* a one-year TTL while keeping /index.html on a 60-second DefaultTTL so that deployments that only update the HTML propagate quickly even without an explicit invalidation.
CloudFront Invalidation Lifecycle Diagram
Verification
After submitting an invalidation, confirm the edge is now serving fresh content. The x-cache response header from CloudFront tells you exactly what happened at the edge.
Check cache status for a specific asset:
curl -sI "https://d1234abcd.cloudfront.net/index.html" | grep -i "x-cache\|x-amz-cf-pop\|age\|cache-control"
Immediately after invalidation you should see:
x-cache: Miss from cloudfront
age: 0
cache-control: max-age=60
x-amz-cf-pop: IAD89-P3
On subsequent requests the header transitions to:
x-cache: Hit from cloudfront
age: 14
Check invalidation status by ID:
INVALIDATION_ID=I1ABCDEFGHIJKL
aws cloudfront get-invalidation \
--distribution-id "$DIST_ID" \
--id "$INVALIDATION_ID" \
--query "Invalidation.{Status:Status,CreateTime:CreateTime}" \
--output table
List all invalidations on a distribution:
aws cloudfront list-invalidations \
--distribution-id "$DIST_ID" \
--query "InvalidationList.Items[*].{ID:Id,Status:Status,CreateTime:CreateTime}" \
--output table
Check how many are currently in progress (limit is 15 concurrent):
aws cloudfront list-invalidations \
--distribution-id "$DIST_ID" \
--query "length(InvalidationList.Items[?Status=='InProgress'])" \
--output text
Confirm a fingerprinted asset bypasses the need entirely:
# New deploy produces app.a1b2c3d4.js — verify the new path is live
curl -sI "https://d1234abcd.cloudfront.net/assets/app.a1b2c3d4.js" | grep "x-cache"
# Expected: Miss from cloudfront (first request), then Hit from cloudfront
Edge Cases and Known Issues
The 15-concurrent-invalidation limit. CloudFront allows at most 15 in-progress invalidation requests per distribution at once. In high-velocity CI environments where multiple deploy pipelines run in parallel this limit is easy to hit. Mitigate it by batching all affected paths into a single create-invalidation call rather than one call per file, or by queuing deploys so invalidations do not pile up. The boto3 example above raises a clear RuntimeError when this limit is hit.
Wildcard patterns are prefix-anchored, not glob patterns. /assets/* invalidates everything under /assets/ but /assets/app.*.js is not a valid pattern — CloudFront wildcard syntax only supports a trailing *. You cannot use wildcards in the middle of a path. If you need to invalidate a subset of files with a shared prefix, structure your paths accordingly: /assets/js/*, /assets/css/*.
CallerReference uniqueness. If a boto3 call is retried (network timeout, Lambda retry) with the same CallerReference, CloudFront returns the existing invalidation response rather than creating a new one. This is intentional idempotency, but if you are building a retry loop you must track whether the returned invalidation is already Completed or was freshly created.
OAC and SSE-KMS. If your S3 bucket uses SSE-KMS encryption, the OAC service principal must have kms:Decrypt permission on the KMS key. Without it, CloudFront receives a 403 from S3, caches the error (briefly), and the invalidated path returns an error page rather than the updated object. Add the CloudFront service principal to the key policy.
MinTTL > 0 overrides Cache-Control. If you have set MinTTL to a non-zero value in a cache behavior, CloudFront ignores the origin’s Cache-Control: max-age and Expires headers and caches for at least that many seconds. This means that even after a successful invalidation, the very next request re-populates the cache for MinTTL seconds. For files you need to update frequently, keep MinTTL=0 and rely on the Cache-Control header from S3. See Cache-Control immutable and TTL tuning for the full picture.
Edge locations vs regional edge caches. CloudFront has two cache tiers: edge POPs and regional edge caches (RECs). An invalidation propagates to both. However, after invalidation, the next request hits the POP, misses, checks the REC, misses there too (if it was also invalidated), and only then fetches from origin. This double-miss means your origin may see a brief spike in requests across all regions simultaneously after a wildcard invalidation on a popular distribution.
S3 eventual consistency and invalidation timing. S3 is strongly consistent as of December 2020 for new objects and overwrites. A PutObject that completes before you call create-invalidation will be reflected correctly when CloudFront re-fetches. However, if you overwrite an S3 object and immediately invalidate without confirming the PutObject succeeded (e.g., in a pipeline with async steps), there is a race window where CloudFront might fetch the old object. Always ensure S3 writes complete before triggering invalidation.
Path case sensitivity. CloudFront path matching is case-sensitive. /Index.html and /index.html are different cache keys. Make sure your invalidation paths exactly match the paths your application uses.
Performance Impact
Invalidation propagation latency is the time between submitting the API call and all edge POPs having evicted the object. In practice this is typically 30–60 seconds for most regions. During propagation, some edge nodes serve stale content and others serve fresh content. For most web applications this is acceptable. For content with strict consistency requirements (payment confirmations, legal notices) consider serving those paths from the origin directly using a no-cache cache behavior rather than relying on invalidation.
Origin traffic spike. When a wildcard invalidation completes, every POP that receives a request for a matching path in the next few seconds makes an origin fetch. On a distribution serving hundreds of millions of monthly requests, this can mean thousands of simultaneous S3 GETs. S3 scales horizontally but if your origin has rate limits or you are using a custom HTTP server, add cache stampede protection: use shorter DefaultTTL values for frequently-changing paths rather than invalidating after every deploy.
Fingerprinted assets have zero performance cost. A fingerprinted asset — /assets/app.a1b2c3d4.js — is cached by CloudFront with max-age=31536000. When you publish a new build, the new hash produces a new URL. CloudFront treats it as a brand new object; no invalidation is submitted; the old URL naturally expires from edge caches when the TTL lapses (or immediately if it drops out of the LRU). User browsers that have the old HTML also have the old JS filename and serve the cached version locally — zero origin requests. Only users who load the new HTML get the new filename and fetch the new asset. This is covered in depth under content hashing.
Cost scaling. For a site with 50 deploys per month invalidating /index.html and 3 other files (4 paths per invalidation), monthly path count is 200 — well within the free 1,000 paths. At 500 deploys per month with 10 paths each that is 5,000 paths: 4,000 billable paths × $0.005 = $20/month. For very high-frequency deployments it is worth switching the non-HTML paths to fingerprinted filenames to avoid the accumulating cost entirely.
FAQ
What is the difference between an invalidation path and an invalidated object?
A path is what you submit in the API call; an object is each unique cached file that path matches. The path /assets/* is one path but might match 500 objects. CloudFront charges per path, not per object — so a wildcard invalidation that evicts 10,000 objects costs the same as one that evicts a single file. The free tier covers 1,000 paths per month; after that each additional path costs $0.005.
Can I use invalidation to roll back a bad deployment?
Yes, with caveats. If you overwrote /assets/app.js in S3 with a broken version, you can restore the previous version in S3 and then invalidate /assets/app.js to force CloudFront to re-fetch. But if your assets are fingerprinted, the broken file has a unique hash in its name (e.g., /assets/app.bad0cafe.js) while the previous good file is at a different URL (e.g., /assets/app.a1b2c3d4.js). Rolling back then means updating /index.html to reference the good URL and invalidating /index.html — one path, instant, zero risk of serving the broken asset again.
Why does my invalidation complete but users still see stale content?
Browser-level caching. If you served /index.html with Cache-Control: max-age=3600, the user’s browser cached it for an hour regardless of what you do on CloudFront. CloudFront invalidation only affects the edge cache; it cannot reach into user browsers. This is why HTML files should use short TTLs (max-age=60 or no-cache) while fingerprinted assets use long TTLs. See Cache-Control: immutable for the correct header strategy for each asset type.
How many hash characters should I use for fingerprinted filenames?
The convention is 8 hex characters (4 bytes of hash, 1 in 4 billion collision probability per content-identical pair). For monorepos or projects with thousands of chunks — where the birthday paradox makes collisions more likely — use 12–16 hex characters. Most build tools default to 8 (Vite, webpack) but expose a hashLength or [contenthash:12] option. The hash function is typically MD4 (webpack) or SHA-256 truncated (Vite, Rollup); any of these is collision-resistant at 8 characters for typical project sizes. The cache key architecture page covers the tradeoffs between hash length, filename length, and CDN key uniqueness guarantees.
Related
- CDN purge strategies — overview of all CDN invalidation and purge approaches
- Cloudflare cache rules and purge — Cloudflare’s equivalent of CloudFront invalidations, with instant purge by tag
- Fastly instant purge — surrogate-key based purge with sub-second propagation
- Cache-Control immutable and TTL tuning — setting per-path TTL policies in CloudFront cache behaviors