Rolling Back Cache Keys After a Bad Deploy
A bad asset ships. Users report a broken page, a white screen, or a JavaScript error that did not exist in the previous release. The deployed HTML now references app.deadbeef.js — a fingerprinted file that is live on the CDN and in user browsers. The safest recovery is not a CDN purge of that file; it is re-pointing the HTML entry points to the previous hashed filename so that the old, known-good asset resumes being requested.
This page covers how to execute that rollback, what CDN behaviour to expect on Cloudflare, CloudFront, and Nginx, how to verify the recovery, and when to roll forward instead. For CI/CD-level rollback automation see the rollback guide for fingerprinted assets in CI/CD.
Symptom and Mental Model
The symptom is always visible to users before it is visible in monitoring: the HTML served to browsers references a broken asset. The cache key architecture of fingerprinted deployments means:
- The broken
app.deadbeef.jshas its own unique URL and its own CDN cache entry. - The previous
app.a1b2c3d4.jsalso has its own cache entry — it was never purged because the filename changed on deploy, making the old URL simply unreferenced rather than invalid. - Users who already have
app.a1b2c3d4.jsin their browser cache are not affected. Users who have the new HTML (referencingdeadbeef) are broken.
The rollback therefore has two components: (a) stop serving the broken HTML, and (b) confirm the old asset is still warm in the CDN.
Clarification: Why There Is No Hash Collision to Worry About
A fingerprinted filename encodes the content at the moment of the build. Rolling back means re-deploying the previous HTML, which already contains the correct hash (a1b2c3d4) for the previously built asset. There is no risk of collision with the broken asset (deadbeef) because both URLs exist independently in the CDN.
The only scenario requiring active cache management is if you must serve the broken HTML to no one — meaning you need the stale-HTML-serving CDN edge nodes to stop returning the broken document. That requires purging the HTML, not the assets.
Decision Matrix: Roll Back vs Roll Forward
| Factor | Roll Back (redeploy previous release) | Roll Forward (hotfix in new deploy) |
|---|---|---|
| Time to recovery | Seconds to minutes — no new build needed | Minutes to tens of minutes — requires a build |
| Risk | Low — known-good state | Medium — hotfix may introduce regressions |
| CDN asset state | Old asset already cached; HTML swap is instant | New asset must propagate to all edge nodes |
| Broken asset cache entry | Becomes unreferenced; expires via TTL | Coexists harmlessly |
| When to choose | Any incident where root cause is unclear | Bug is trivially identified and fix is certain |
| CI/CD prerequisite | Previous build artifacts still accessible | Normal build pipeline availability |
Default to roll back. A roll forward during an active incident requires confidence in the fix that is rarely available under pressure.
Step-by-Step Rollback Procedure
Step 1: Identify the Previous Hashed Filenames
Locate your previous release’s asset manifest. Depending on your build tool:
# Vite: manifest lives at .vite/manifest.json inside the dist directory
# If you store build artifacts in CI, retrieve the previous run's artifact:
cat dist-previous/.vite/manifest.json | python3 -m json.tool | grep "file"
# Webpack: retrieve the previous webpack-manifest.json or stats.json
cat dist-previous/asset-manifest.json
# Generic: list files by modification time to confirm the previous hashes
ls -lt dist-previous/assets/*.js | head -20
If you use a deployment platform that stores artifact history (GitHub Actions artifacts, S3 versioning, Netlify deploy history), retrieve the previous release’s dist/ output directly.
Step 2: Re-deploy the Previous HTML
The critical file to replace is the HTML entry point (usually index.html) that contains <script src="..."> and <link rel="stylesheet" href="..."> tags referencing the broken hashes.
# Example: restore previous index.html from a tagged Git release
git show v2.4.0:dist/index.html > dist/index.html
# Then re-deploy only the HTML to your CDN origin or static host
# (the assets themselves do not need to be re-uploaded — they are already cached)
# Cloudflare Pages: push the previous commit or redeploy via API
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/pages/projects/$PROJECT_NAME/deployments" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"branch": "main"}'
# Netlify: redeploy a previous deploy ID
curl -X POST "https://api.netlify.com/api/v1/sites/$SITE_ID/deploys/$PREVIOUS_DEPLOY_ID/restore" \
-H "Authorization: Bearer $NETLIFY_TOKEN"
For server-rendered or self-hosted deployments, replace the symlink that points to the active release:
# Atomic symlink swap — no downtime
ln -sfn /var/www/releases/v2.4.0 /var/www/current
# Reload the web server to pick up the symlinked change
systemctl reload nginx
Step 3: Purge the HTML Cache Entry
The assets do not need purging — only the HTML does. Purge the minimum set of URLs that serve the entry point HTML.
Cloudflare — purge specific HTML URLs:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"files": ["https://example.com/", "https://example.com/index.html"]}'
CloudFront — create an invalidation for the HTML path only:
aws cloudfront create-invalidation \
--distribution-id "$CF_DISTRIBUTION_ID" \
--paths "/" "/index.html"
CloudFront invalidations cost $0.005 per path after the first 1,000 per month. Targeting only the HTML (one or two paths) keeps cost negligible. Do not invalidate /assets/* — those objects are immutable and have not changed.
Nginx proxy cache — purge the HTML entry point only:
# If using nginx-cache-purge module (ngx_cache_purge):
curl -X PURGE "https://example.com/"
curl -X PURGE "https://example.com/index.html"
# Without the purge module, move the cache file and reload:
find /var/cache/nginx -name "*.cache" -newer /var/www/current/index.html -delete
nginx -s reload
Step 4: Confirm the Old Asset Is Still Warm
Before the rollback completes, verify the previous good asset is still accessible at its original URL:
GOOD_HASH="a1b2c3d4"
ASSET_PATH="assets/main-${GOOD_HASH}.js"
curl -sI "https://example.com/${ASSET_PATH}" \
| grep -E "^(http|cf-cache-status|x-cache|cache-control|age):" -i
Expected output:
HTTP/2 200
cache-control: public, max-age=31536000, immutable
cf-cache-status: HIT
age: 172800
A HIT with a positive Age: confirms the old asset is still warm in the CDN. A MISS means the TTL expired (unusual within 24 hours of a deploy) or the asset was never deployed to this CDN edge. In the rare MISS case, the origin will re-serve the file if you kept the old asset in origin storage — which you should always do for at least one release cycle.
Verification Command
After completing Steps 1–4, run a single end-to-end check:
#!/bin/bash
# rollback_verify.sh
# Usage: DOMAIN=example.com GOOD_HASH=a1b2c3d4 ASSET_NAME=main bash rollback_verify.sh
set -euo pipefail
DOMAIN="${DOMAIN:?set DOMAIN}"
GOOD_HASH="${GOOD_HASH:?set GOOD_HASH}"
ASSET_NAME="${ASSET_NAME:?set ASSET_NAME}"
ASSET_URL="https://${DOMAIN}/assets/${ASSET_NAME}-${GOOD_HASH}.js"
HTML_URL="https://${DOMAIN}/"
echo "=== Verifying HTML references prior good hash ==="
curl -s "$HTML_URL" | grep -o "${ASSET_NAME}-[a-f0-9]\{8\}\.js" | sort -u
echo ""
echo "=== Verifying asset is still cached ==="
curl -sI "$ASSET_URL" | grep -E "^(http|cf-cache-status|x-cache|cache-control|age):" -i
echo ""
echo "=== Verifying broken asset no longer referenced ==="
BROKEN_COUNT=$(curl -s "$HTML_URL" | grep -c "deadbeef" || true)
echo "References to broken hash in HTML: $BROKEN_COUNT (expect 0)"
Running this script should show the HTML pointing to the good hash, the CDN returning a HIT for that asset, and zero references to the broken hash.
When to Reconsider (Roll Forward Instead)
Roll forward — fixing the bug and deploying a new release — is appropriate when:
- The bug is a single-line fix with zero risk of regression and the build pipeline is fast (under two minutes). Rolling forward resolves the incident cleanly rather than leaving an unreferenced broken asset in CDN storage.
- The previous release also had a bug that you are rolling back to. A rollback to a previously known-good state is safe; a rollback to a state that also had problems trades one incident for another.
- The breakage is in the HTML template itself rather than the asset content. A new deploy with the corrected template is simpler than re-deploying an old HTML version that may also contain the template error.
- You are mid-migration between query-parameter versioning and filename hashing. In that case, the old filenames may not exist on the CDN because the previous strategy used different cache keys. Confirm old assets are accessible before committing to a rollback path.
CDN-Specific Considerations
Cloudflare: HTML purge is near-instant globally via the API. Old fingerprinted assets cached at the edge remain available until their max-age TTL expires — typically one year. No action needed for the assets.
CloudFront: Invalidation propagation takes 10–60 seconds globally. Until the invalidation completes, some edge nodes may still serve the broken HTML. To minimise exposure, include both / and /index.html in the invalidation. The old asset remains cached at every edge node where it was requested — it is never actively evicted during normal operation.
Nginx (self-hosted): Proxy cache purge requires either the ngx_cache_purge module or a manual cache-file deletion. The most reliable approach is an atomic symlink swap (ln -sfn) for the serving directory followed by nginx -s reload, which clears the active configuration without downtime. Old asset files on disk at the origin remain accessible for the duration of their CDN TTL.
Related
- Cache key architecture — parent reference: how CDN keys are constructed and why old assets remain cached
- Implementing cache keys: query parameters vs filenames — choosing the strategy that minimises rollback complexity
- Rolling back fingerprinted assets in CI/CD — automating rollback through your pipeline
- Cloudflare cache rules and purge — CDN purge operations for HTML invalidation during rollback