Rolling Back a Bad Asset Deploy on CloudFront
A broken JavaScript bundle shipped. Users see a blank page or a runtime error. The fix is already staged but the current release is live and CloudFront is serving it globally. This guide walks through the fastest safe rollback path: restore the prior index.html from S3, invalidate that one path, and rely on the fact that all prior hashed assets are still in S3 and still cached at the edge.
Why CloudFront Rollbacks Are Simpler With Fingerprinted Assets
The key insight behind a surgical rollback is that content hashing makes each release’s assets independent objects in S3. When you deployed main.a1b2c3d4.js in the bad release and main.9e2f1a3b.js was the prior good release, both files exist in S3 and both may still be cached at CloudFront edge POPs. Rolling back means pointing index.html back to the old hash — the file it references is already present.
Contrast this with non-fingerprinted deployments, where main.js is overwritten in-place. There, rolling back requires re-uploading the old binary and then invalidating /* to evict all stale copies — a much noisier operation that triggers a cache-miss storm.
Fingerprinting also protects against the most common rollback hazard: a mismatch between HTML and assets. When index.html references main.9e2f1a3b.js, that exact file is served from S3 (or from an edge POP that still has it cached). There is no window where users get new HTML with old files or old HTML with new files.
Review cache key architecture for the underlying reason: each unique filename is a distinct cache entry. The old hash URL and the new hash URL coexist in the edge cache simultaneously. Only index.html — which is not fingerprinted — needs any attention at all.
Choosing a Rollback Strategy
| Dimension | Nuclear cache wipe (/*) |
Surgical HTML rollback (/index.html only) |
|---|---|---|
| What gets invalidated | Every object in the distribution | HTML entry points only |
| Cache-miss storm risk | High — all POPs must refetch everything | Minimal — only HTML POPs refetch |
| Invalidation path cost | 1 path (/* counts as one) |
1–3 paths |
| Time to user recovery | 5–30 s invalidation + cache warm-up | 5–30 s invalidation; assets already warm |
| Works with fingerprinted assets | Yes, but wastes edge cache | Yes, optimal |
| Works with unhashed assets | Required if assets were overwritten | No — same URL, old content still cached |
| Risk of partial-state exposure | Moderate during warm-up | Very low — old hashed assets serve immediately |
Use the surgical approach whenever your assets are fingerprinted. Use the nuclear approach only when assets are not fingerprinted (same URL, new content) or when a security-sensitive payload must be purged immediately from all edge caches.
Step-by-Step Rollback Procedure
The complete rollback script below handles all five steps: identifying the prior S3 version, restoring it, re-uploading with correct cache headers, invalidating index.html, and polling for completion.
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------
# CloudFront rollback: restore prior index.html and invalidate the path.
# Prerequisites: AWS CLI v2, correct IAM permissions, DIST_ID and BUCKET set.
# -----------------------------------------------------------------------
DIST_ID="${DIST_ID:?Set DIST_ID to your CloudFront distribution ID}"
BUCKET="${BUCKET:?Set BUCKET to your S3 bucket name}"
HTML_KEY="index.html"
PRIOR_HTML="/tmp/prior-index.html"
echo "=== Step 1: List recent S3 versions of ${HTML_KEY} ==="
aws s3api list-object-versions \
--bucket "$BUCKET" \
--prefix "$HTML_KEY" \
--query "Versions[*].{VersionId:VersionId,LastModified:LastModified,IsLatest:IsLatest}" \
--output table
# Set PRIOR_VERSION_ID to the version you want to restore.
# Example: export PRIOR_VERSION_ID="abc123XYZ..."
PRIOR_VERSION_ID="${PRIOR_VERSION_ID:?Export PRIOR_VERSION_ID after reviewing the table above}"
echo "=== Step 2: Download prior version to local disk ==="
aws s3api get-object \
--bucket "$BUCKET" \
--key "$HTML_KEY" \
--version-id "$PRIOR_VERSION_ID" \
"$PRIOR_HTML"
echo "=== Step 3: Re-upload with no-cache headers ==="
aws s3 cp "$PRIOR_HTML" "s3://${BUCKET}/${HTML_KEY}" \
--cache-control "no-cache, must-revalidate" \
--content-type "text/html" \
--metadata-directive REPLACE
echo "=== Step 4: Create invalidation for /index.html only ==="
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/index.html" \
--query "Invalidation.Id" \
--output text)
echo "Invalidation ID: $INVALIDATION_ID"
echo "=== Step 5: Wait for invalidation to complete ==="
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 "Rollback complete. Invalidation propagated to all edge POPs."
break
fi
sleep 5
done
echo "=== Verify ==="
echo "Run: curl -sI https://your-distribution.cloudfront.net/index.html | grep -i 'x-cache'"
echo "Expected: x-cache: Miss from cloudfront (first hit), then Hit from cloudfront"
Using S3 Versioning
S3 object versioning must be enabled on the bucket before you deploy. If you have not enabled it, you cannot use list-object-versions to restore a prior HTML file. Enable it with:
aws s3api put-bucket-versioning \
--bucket "$BUCKET" \
--versioning-configuration Status=Enabled
Once enabled, every s3 cp or s3 sync creates a new version. To restore without downloading locally:
# Restore a specific version in-place (no local file needed)
aws s3api copy-object \
--copy-source "${BUCKET}/${HTML_KEY}?versionId=${PRIOR_VERSION_ID}" \
--bucket "$BUCKET" \
--key "$HTML_KEY" \
--cache-control "no-cache, must-revalidate" \
--content-type "text/html" \
--metadata-directive REPLACE
If S3 versioning is not enabled, roll back from your Git repository instead — check out the prior index.html from the previous release tag and re-upload it.
Verification
After the invalidation completes, confirm that users receive the prior HTML:
# Check x-cache header — should be Miss on first request after invalidation
curl -sI "https://your-distribution.cloudfront.net/index.html" \
| grep -iE "x-cache|age|cache-control|etag"
# Expected on first request:
# x-cache: Miss from cloudfront
# age: 0
# cache-control: no-cache, must-revalidate
# Confirm the HTML references the old asset hash (not the broken release hash)
curl -s "https://your-distribution.cloudfront.net/index.html" \
| grep -o 'main\.[a-f0-9]\{8\}\.js'
# Expected: main.9e2f1a3b.js (the prior-release hash)
Verify the old hashed asset itself is still reachable (it was never invalidated):
curl -sI "https://your-distribution.cloudfront.net/assets/main.9e2f1a3b.js" \
| grep -iE "x-cache|age"
# Expected: x-cache: Hit from cloudfront (still warm in edge cache)
# age: some positive number — it never left the cache
If the old asset is not in the edge cache (age: 0, x-cache: Miss), CloudFront will fetch it from S3. As long as the file still exists in S3, the rollback is safe — the edge simply refills its cache from the origin on the next request.
When to Reconsider the Surgical Approach
Non-fingerprinted assets were overwritten in S3. If you deployed new content to main.js (no hash in the filename), the old file is gone from S3. A surgical rollback cannot work because the URL you are rolling back to points to the overwritten file. You must re-upload the old binary to main.js and then invalidate /* to evict stale copies. This is a strong argument for switching to content-addressed filenames — the content-hashing vs semantic-versioning reference explains the trade-offs.
A security-sensitive payload was exposed. If a release accidentally shipped credentials, tokens, or private data in a JavaScript bundle, a surgical rollback that only changes index.html leaves the compromised bundle cached at edge POPs until its TTL expires. In this case, you must also invalidate the specific hashed asset path to evict it from the edge, even though hashed assets normally never need invalidation.
S3 versioning was not enabled. If you do not have a prior version of index.html in S3, you need to reconstruct it from Git or an artifact store. The CI/CD rollback guide covers how to re-run the build from a prior commit and redeploy from the artifact.
Your HTML files are themselves fingerprinted. Some frameworks (Next.js App Router, for example) fingerprint even the HTML file names. In that case, the deployment places the new HTML at a new URL and the root route (/) redirects to it. The rollback procedure is the same conceptually — restore the redirect to point at the prior HTML URL — but the mechanism differs by framework.
The rollback is for a Cloudflare-hosted site. The procedure described here is specific to S3 + CloudFront. For Cloudflare, see rolling back a bad asset deploy on Cloudflare, which uses cache-tag purge rather than path invalidation.
Related
- AWS CloudFront invalidation guide — full invalidation reference: CLI, API, cache behaviors, TTL, cost model
- CDN purge strategies — overview of purge mechanisms across CloudFront, Cloudflare, Fastly, and Nginx
- Rolling back fingerprinted assets in CI/CD — pipeline-level rollback: re-running from a prior artifact without touching the CDN directly
- Rolling back a content-hashed release — the general rollback pattern independent of CDN provider
- Rolling back a bad asset deploy on Cloudflare — equivalent procedure for Cloudflare-hosted sites