Rolling Back a Bad Asset Deploy on Fastly

A bad frontend deploy announces itself fast: 404s for JavaScript bundles, blank screens from broken CSS, or hydration errors because stale HTML references chunk filenames that no longer exist at the origin. When you’re running fingerprinted assets behind Fastly, the rollback path is cleaner than on most CDNs — but only if you structured your surrogate key tagging correctly on the way in. This guide covers the exact sequence: detecting the failure, soft-purging the bad release tag, redeploying prior assets to origin, and verifying that edge nodes are serving the previous version.

Diagnosing a Bad Deploy

Before touching the CDN, confirm the failure mode. The three most common symptoms map to three distinct root causes:

404s on hashed JS/CSS files usually mean the HTML entry point (index.html) was deployed to origin but the hashed asset files were not — or the assets were deleted before the HTML was reverted. Fastly is serving a cached index.html that references filenames the origin no longer has.

JavaScript runtime errors or blank screens after a deploy that appeared to succeed often indicate a chunk manifest mismatch: dynamic imports in the entry bundle reference hashed filenames for lazy-loaded chunks that were not deployed, or were deployed with a different hash than the manifest recorded.

Stale CSS applying old styles to new markup is the inverse problem: the HTML shipped but an intermediate Fastly node cached the prior CSS bundle and has not yet fetched the new one. This resolves itself when the cached object’s TTL expires, but a targeted soft purge is faster.

In each case the first diagnostic step is the same — inspect the response headers on the affected asset:

curl -sI https://your-service.global.ssl.fastly.net/assets/app-a1b2c3d4.js \
  | grep -Ei "^(x-cache|surrogate-key|age|cache-control|content-length):"

Key signals:

  • X-Cache: HIT with a high Age: value — Fastly has a cached copy; the origin state is irrelevant until this object is purged.
  • X-Cache: MISS — Fastly is fetching from origin every request; the problem is at the origin, not the CDN.
  • Surrogate-Key: release-v2-5-1 assets js — confirms which tag group this object belongs to, which is what you’ll target in the soft purge.

If Surrogate-Key is absent, your origin is not emitting the header. Fix that first — you cannot use tag-based purge without it.

Soft Purge vs. Hard Purge

The choice between soft and hard purge determines whether users experience a gap in availability during rollback. Fastly’s instant purge API supports both modes on the same endpoint; the difference is a single request header.

Soft Purge Hard Purge
Behavior Marks cached objects stale; Fastly continues serving them while asynchronously revalidating against origin Immediately evicts objects from cache; next request goes directly to origin with no cached fallback
Gap in service? No — stale content serves until fresh content is cached Yes — if origin is slow or errors, users see failures
Latency during rollback Low; edge keeps serving stale v2.5.1 while origin returns v2.5.0 assets Variable; every request blocks on origin until new objects are cached
When to use Rollback to a known-good prior release where origin will be ready Emergency purge of malicious or legally sensitive content that must not be served at all
Risk Users may briefly see mixed stale v2.5.1 + fresh v2.5.0 if HTML and assets are purged out of order Origin overload spike; users see errors if origin is under capacity

For a routine bad-deploy rollback, soft purge is almost always correct. You are not trying to hide the content — you are racing to get a working version to users with zero downtime.

The Rollback Timeline

Fastly rollback timeline A six-step sequence showing how a bad v2.5.1 deploy is detected, soft-purged, and replaced by v2.5.0 assets flowing through Fastly's edge cache back to users. Fastly Soft-Purge Rollback: v2.5.1 → v2.5.0 ① Deploy ② Detect ③ Soft Purge ④ Stale Served ⑤ Origin Ready ⑥ Fresh Cache v2.5.1 cached BROKEN 404 / JS err detected Soft purge release-v2-5-1 Stale v2.5.1 served (no gap) origin fetch bg v2.5.0 at origin deployed & live v2.5.0 cached X-Cache: HIT Origin v2.5.1 assets serving (bad HTML + JS) steps ①–③ Rollback deploy in progress upload v2.5.0 assets → update index.html v2.5.0 assets live at origin steps ⑤–⑥ Users Errors / broken experience (v2.5.1) No gap — stale then fresh v2.5.0 delivered
Fastly soft-purge rollback sequence: stale v2.5.1 continues serving during the revalidation window while v2.5.0 assets are restored at origin, eliminating any gap in CDN availability.

Step-by-Step Rollback

1. Soft-Purge the Bad Release Tag

Every asset deployed in a release should carry a Surrogate-Key response header that includes a release-scoped tag — for example release-v2-5-1. Tagging conventions are covered in the Fastly surrogate keys guide. With that tag in place, a single API call marks every object from the bad release as stale across all Fastly edge nodes simultaneously:

curl -X POST "https://api.fastly.com/service/$SERVICE_ID/purge/release-v2-5-1" \
  -H "Fastly-Key: $FASTLY_TOKEN" \
  -H "Fastly-Soft-Purge: 1" \
  -H "Accept: application/json"

Fastly-Soft-Purge: 1 is the critical header. Without it the request is a hard purge. The API responds with {"status":"ok"} within milliseconds once the purge has propagated — Fastly’s instant purge infrastructure completes this globally in under 150 ms.

Do not purge individual asset URLs. Do not purge by wildcard path. Tag-based purge is the only approach that atomically covers every node without walking a file list.

2. Deploy Prior Build Assets to Origin

While Fastly serves stale v2.5.1 assets (with no visible gap to users), race to restore the v2.5.0 artifacts at your origin. The exact mechanism depends on your deployment target:

# Example: restore v2.5.0 from a prior build artifact stored in S3/R2/GCS
# Replace with your actual object storage CLI and bucket paths

aws s3 sync \
  s3://your-build-artifacts/release-v2.5.0/assets/ \
  s3://your-origin-bucket/assets/ \
  --cache-control "public, max-age=31536000, immutable" \
  --metadata-directive REPLACE

# After assets are in place, restore the HTML entry point last
aws s3 cp \
  s3://your-build-artifacts/release-v2.5.0/index.html \
  s3://your-origin-bucket/index.html \
  --cache-control "public, max-age=0, must-revalidate" \
  --content-type "text/html; charset=utf-8" \
  --metadata-directive REPLACE

The order matters: assets before HTML. If you restore index.html first and it references app-b3c4d5e6.js (the v2.5.0 filename), but that file is not yet at origin, any Fastly node that revalidates during that window will get a 404 from origin and cache the error response. Assets are content-addressed by their hash, so restoring them is safe at any time — no user sees them until index.html starts pointing to them.

3. Why Fingerprinted Assets Make This Easy

This is where the content-hashing approach pays off in an incident. Because app-a1b2c3d4.js (v2.5.0) and app-b3c4d5e6.js (v2.5.1) are different filenames, neither was ever evicted from Fastly during the forward deploy. Fastly cached v2.5.0’s assets when they were first deployed, and they remained cached — untouched — while v2.5.1 was deployed alongside them.

Rollback therefore requires no new asset uploads in many cases. If Fastly still has app-a1b2c3d4.js in cache from the original deploy (and the TTL has not expired), restoring index.html to point back to that filename is sufficient. The CDN serves the old hashed asset from its existing cache entry.

This is fundamentally different from a path-based deployment (/assets/app.js) where every deploy overwrites the same URL and the CDN must be purged — and rolled forward — just to get back to a prior state. The cache key architecture guide explains the structural reasons why content-addressed filenames isolate releases.

4. Redeploy the HTML Entry Point

index.html is the only mutable URL in a fingerprinted deployment. It carries Cache-Control: no-cache or a short TTL specifically so it can be updated without a purge. After restoring v2.5.0’s index.html at origin, trigger a targeted purge or simply let the short TTL expire. If your stale-while-revalidate policy on HTML entry points is correctly configured, the update will propagate to users within your configured stale-while-revalidate window — typically 60 seconds.

If you need index.html to update immediately (zero-delay rollback), issue a direct URL purge — without Fastly-Soft-Purge: 1 — just for the HTML file:

curl -X PURGE "https://api.fastly.com/purge/your-domain.com/index.html" \
  -H "Fastly-Key: $FASTLY_TOKEN"

HTML is small and revalidates fast; the one-request gap in cache is acceptable.

Verification

After the rollback, confirm both the CDN state and the origin state:

curl -sI https://your-service.global.ssl.fastly.net/assets/app-a1b2c3d4.js | grep -E "^(x-cache|surrogate-key|age|cache-control):" -i

Expected output for a successfully cached v2.5.0 asset:

x-cache: HIT, HIT
surrogate-key: release-v2-5-0 assets js
age: 47
cache-control: public, max-age=31536000, immutable

The sequence to watch for after issuing the soft purge:

  1. First few requests after purge: X-Cache: MISS — Fastly is revalidating. The response is still delivered from the stale object while the background fetch completes; users see content.
  2. Once origin has returned fresh content: X-Cache: HIT with a low Age: and the surrogate-key reflecting the v2.5.0 tag.

If you see surrogate-key: release-v2-5-1 on a response that should be v2.5.0, the origin is still serving v2.5.1 assets — the origin-side rollback is incomplete.

How do I tell if the soft purge propagated to all edge nodes?

The X-Served-By header in Fastly responses identifies the specific POP and cache node that served the response. After a purge, send requests from multiple geographic locations (or use the Fastly-Debug: 1 request header, which Fastly honors on test traffic) and confirm all Age: values drop to near zero or X-Cache shows MISS followed by HIT.

What if I cannot find the old build artifact?

If the v2.5.0 artifact was not stored in a build cache or object storage bucket, you need to re-run the v2.5.0 build from source. This is the strongest argument for storing every production build artifact immutably — even a 30-day retention window is sufficient to cover the rollback window for any realistic release cycle. The CI/CD asset pipeline integration guide covers artifact retention configuration for GitHub Actions and common CD systems.

CI/CD Integration: Triggering Rollback from GitHub Actions

Manual curl commands work in a crisis but a rehearsed automated rollback is faster and less error-prone. The following workflow can be triggered manually via workflow_dispatch with the target release tag as an input:

name: Fastly Rollback

on:
  workflow_dispatch:
    inputs:
      rollback_to_version:
        description: "Version to restore (e.g. v2.5.0)"
        required: true
      bad_release_tag:
        description: "Surrogate key tag to soft-purge (e.g. release-v2-5-1)"
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - name: Soft-purge bad release from Fastly
        run: |
          curl -sf -X POST \
            "https://api.fastly.com/service/${{ secrets.FASTLY_SERVICE_ID }}/purge/${{ github.event.inputs.bad_release_tag }}" \
            -H "Fastly-Key: ${{ secrets.FASTLY_TOKEN }}" \
            -H "Fastly-Soft-Purge: 1" \
            -H "Accept: application/json"

      - name: Restore prior assets from artifact store
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: us-east-1
          ROLLBACK_VERSION: ${{ github.event.inputs.rollback_to_version }}
        run: |
          aws s3 sync \
            "s3://${{ secrets.BUILD_ARTIFACT_BUCKET }}/releases/${ROLLBACK_VERSION}/assets/" \
            "s3://${{ secrets.ORIGIN_BUCKET }}/assets/" \
            --cache-control "public, max-age=31536000, immutable" \
            --metadata-directive REPLACE

          aws s3 cp \
            "s3://${{ secrets.BUILD_ARTIFACT_BUCKET }}/releases/${ROLLBACK_VERSION}/index.html" \
            "s3://${{ secrets.ORIGIN_BUCKET }}/index.html" \
            --cache-control "public, max-age=0, must-revalidate" \
            --content-type "text/html; charset=utf-8" \
            --metadata-directive REPLACE

      - name: Purge HTML entry point
        run: |
          curl -sf -X PURGE \
            "https://api.fastly.com/purge/${{ secrets.ORIGIN_DOMAIN }}/index.html" \
            -H "Fastly-Key: ${{ secrets.FASTLY_TOKEN }}"

      - name: Verify rollback
        run: |
          sleep 5
          STATUS=$(curl -sI "https://${{ secrets.ORIGIN_DOMAIN }}/assets/app-00000000.js" \
            | grep -i "^x-cache:" | head -1)
          echo "Cache status after rollback: $STATUS"

Replace app-00000000.js with an actual filename from the rollback target release. The 8-character hash default is shown here; for monorepos with thousands of chunks, 12–16 hex characters reduce the probability of accidental collisions between releases.

The key precondition this workflow checks implicitly: the artifact must exist in BUILD_ARTIFACT_BUCKET. If aws s3 sync returns a non-zero exit code because the prefix is empty, the step fails visibly and the rollback halts before index.html is updated — which is the safe failure mode.

When to Reconsider a CDN Rollback

Soft-purging Fastly and restoring a prior build at origin solves the asset delivery problem. It does not solve everything.

Database schema changes. If v2.5.1 shipped a migration that removed a column the v2.5.0 frontend depends on, rolling back the frontend assets will restore a UI that sends API requests the current backend cannot satisfy. CDN rollback must be paired with a backend rollback — or the schema change must be reverted first.

Breaking API changes. If v2.5.1 also changed an API response shape that v2.5.0’s JavaScript code relies on, rolling back the CDN layer produces a client that sends requests in the old format to a backend expecting the new format. Coordinate the rollback across both layers.

Authenticated asset URLs. If your Fastly service uses signed URLs or token-based authentication for asset access, the prior index.html may reference signed URLs generated at build time that have since expired. A content-hashing rollback assumes unsigned, publicly cacheable asset URLs.

Roll forward instead. If the defect is a minor visual regression and a fix is ready within minutes, it is often faster to deploy v2.5.2 (with the fix) and purge the v2.5.1 release tag than to orchestrate a full rollback. The CDN purge step is identical; you are just pointing origin forward rather than backward. The rolling back a content-hashed release guide covers the decision matrix for rollback vs. roll-forward in more depth.

When stale serving is not acceptable. Soft purge serves stale content for the revalidation window. If the bad asset is harmful — an exposed secret, incorrect legal text, a security vulnerability in a script — use a hard purge for immediate eviction, accept the brief service gap, and prioritize origin readiness before issuing the purge.