Rolling Back esbuild Fingerprinted Assets After a Bad Deploy

A bad deploy with fingerprinted assets is worse than a bad deploy with static filenames. When something breaks in production, you cannot simply overwrite main.js — the CDN cached main-a1b2c3d4.js at that exact URL for up to a year. The HTML entry point already references that hash. Your users are stuck until you either fix forward or restore the exact prior artifacts under their original hashed filenames.

This guide is an emergency response playbook: detect the bad deploy, use the esbuild metafile to identify prior hashed outputs, redeploy those prior artifacts, update the HTML entry point, and verify the CDN is serving the correct version. Every step is runnable as written.

Before starting, make sure you understand how the esbuild metafile drives fingerprinting workflows — specifically that the outputs map is the authoritative record of which content hash was embedded in each filename. If you are also coordinating a CDN purge as part of this rollback, the Cloudflare cache rules and purge guide covers targeted URL invalidation. For rollbacks managed inside a CI/CD pipeline, see rolling back fingerprinted assets in CI/CD.

How the esbuild Metafile Enables Rollback

When you set metafile: true in your esbuild config, esbuild writes a JSON structure that maps every output filename to its origin. The esbuild 0.20+ metafile structure looks like this:

{
  "inputs": {
    "src/index.js": { "bytes": 1234, "imports": [] }
  },
  "outputs": {
    "dist/index-a1b2c3d4.js": {
      "imports": [],
      "exports": [],
      "entryPoint": "src/index.js",
      "bytes": 45678
    },
    "dist/assets/logo-9c0d1e2f.png": {
      "imports": [],
      "bytes": 8192
    }
  }
}

Three fields matter for rollback:

  • outputs keys — the exact output paths, including the embedded hash. These are the filenames you must redeploy.
  • entryPoint — present only on chunks that map to a named entry; use this to match each output back to its source file when regenerating HTML references.
  • bytes — the artifact size. When you suspect artifact corruption, compare the bytes value from the prior metafile against the actual file size on disk or in your artifact store to confirm you have an unmodified copy.

The rollback strategy depends entirely on finding the prior metafile. If your CI/CD pipeline archives build artifacts — including meta.json — alongside the compiled assets, you can read the prior metafile, reconstruct the exact prior filename list, redeploy those files, and swap the HTML entry point. If artifacts were not preserved, the only option is a full rebuild from the prior commit, which takes longer but produces deterministic outputs identical to the original deploy when your build is correctly configured.

Decision Matrix: Redeploy Prior Artifacts vs. Full Rebuild vs. CDN URL Swap

Approach When to use Speed Risk
Redeploy prior artifacts Artifacts preserved in CI store or S3 bucket Fast (minutes) Low — exact same bytes, no rebuild needed
Full rebuild from prior commit No artifact store; build is deterministic Medium (build time + deploy time) Low if build is deterministic; High if build is non-deterministic
CDN URL swap only Static origin with path aliasing support Very fast Medium — depends on CDN alias propagation; does not fix origin
Fix forward Bug is small and easily patched Medium Low for trivial fixes; increases if the fix introduces new risk
Revert HTML only Assets are fine; only entry-point HTML changed Fast Low — valid when bad deploy only touched index.html

Choose “redeploy prior artifacts” first if your pipeline archives build outputs. It is the most reliable path because it reuses exact bytes without a compiler invocation. Choose “full rebuild from prior commit” when you trust your deterministic build outputs and have no artifact store. Avoid CDN URL swap except as a temporary stopgap — it does not repair the origin and leaves a configuration inconsistency.

esbuild rollback decision flow A flowchart showing five steps: detect bad deploy, locate prior metafile, identify prior hashed filenames from the outputs map, redeploy artifacts and update HTML, then verify CDN response headers. Detect bad deploy errors, 404s, wrong behavior Prior artifacts preserved? check CI store / S3 / artifact registry yes no Rebuild from prior commit Read prior meta.json parse outputs map, note hashed filenames Redeploy assets + update HTML upload prior dist/, deploy prior index.html Verify CDN serves correct version
Rollback decision flow: detect the bad deploy, check for preserved artifacts, read the prior metafile to extract hashed filenames, redeploy, then verify at the CDN edge.

Step 1 — Locate the Prior Metafile

Your CI/CD pipeline should archive dist/meta.json after every successful deploy. If it does, retrieve the prior build’s metafile from your artifact store. If you use GitHub Actions with artifact upload, the artifact will be named and versioned per run.

If the metafile was not archived separately, check whether the entire dist/ directory was archived — the metafile is typically included. As a last resort, if you store build outputs in an S3-compatible store keyed by git SHA, retrieve the prior SHA’s objects directly.

# If using GitHub Actions artifacts (via gh CLI):
gh run list --limit 10 --json databaseId,headBranch,conclusion,displayTitle
# Find the last successful run before the bad deploy, then:
gh run download <PRIOR_RUN_ID> --name build-artifacts --dir /tmp/prior-build

# Confirm the metafile is present:
ls /tmp/prior-build/dist/meta.json

Once you have the prior meta.json, you have everything needed to reconstruct the exact filename list.

Step 2 — Extract Prior Hashed Filenames from the Metafile

Parse the outputs object to build two lists: entry-point outputs (those with an entryPoint field — needed to regenerate the HTML reference) and all other outputs (assets, CSS, lazy chunks — needed for upload).

// scripts/list-prior-assets.mjs
// Usage: node scripts/list-prior-assets.mjs /tmp/prior-build/dist/meta.json
import fs from 'fs';
import path from 'path';

const metaPath = process.argv[2];
if (!metaPath) {
  console.error('Usage: node list-prior-assets.mjs <path-to-meta.json>');
  process.exit(1);
}

const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
const entryOutputs = {};
const allOutputs = [];

for (const [outputPath, info] of Object.entries(meta.outputs)) {
  const filename = path.basename(outputPath);
  allOutputs.push({ outputPath, filename, bytes: info.bytes });

  if (info.entryPoint) {
    const src = path.basename(info.entryPoint);
    entryOutputs[src] = { outputPath, filename };
  }
}

console.log('\n=== Entry outputs (used in HTML) ===');
for (const [src, out] of Object.entries(entryOutputs)) {
  console.log(`  ${src}${out.filename}  (${out.outputPath})`);
}

console.log('\n=== All outputs (upload these) ===');
for (const asset of allOutputs) {
  console.log(`  ${asset.outputPath}  (${asset.bytes} bytes)`);
}

Running this script against the prior metafile prints exactly which hashed filenames need to be present on the origin and which filename the HTML must reference. For example:

=== Entry outputs (used in HTML) ===
  index.js → index-a1b2c3d4.js  (dist/index-a1b2c3d4.js)

=== All outputs (upload these) ===
  dist/index-a1b2c3d4.js  (45678 bytes)
  dist/assets/logo-9c0d1e2f.png  (8192 bytes)

The 8-character hex hash is the default esbuild length. If your monorepo uses thousands of chunks, extend to 12 or 16 characters via entryNames: '[name]-[hash:12]' to reduce collision probability — but do not change hash length between builds, as this changes every output filename.

Step 3 — Redeploy Prior Artifacts

Upload the prior artifacts from your artifact store to the origin. The exact command depends on your storage backend.

# Example: sync prior dist/ to S3-compatible origin
# Assumes /tmp/prior-build/dist/ contains the restored artifacts

aws s3 sync /tmp/prior-build/dist/ s3://your-bucket/assets/ \
  --cache-control "public, max-age=31536000, immutable" \
  --exclude "*.html" \
  --exclude "meta.json"

# Deploy the prior index.html last (no immutable cache — it must be revalidated)
aws s3 cp /tmp/prior-build/dist/index.html s3://your-bucket/ \
  --cache-control "public, max-age=0, must-revalidate" \
  --content-type "text/html; charset=utf-8"

Deploying the HTML last is critical. If you upload the HTML first and the assets are still in transit, users who load the page during that window receive a new HTML pointing at assets that may not exist yet on the CDN edge — the same race condition you are trying to escape. See integrating esbuild with CDN fingerprinting workflows for full sequencing rules.

After uploading, purge only the HTML entry point from the CDN. Because hashed asset URLs are immutable, the CDN’s cached copies of index-a1b2c3d4.js are permanently valid — they do not need purging. Only the mutable index.html URL needs invalidation so edges pull the prior version. Details on scoped purge calls are in the Cloudflare cache purge guide and the Cloudflare bad-deploy rollback walkthrough.

Verification

Confirm the CDN edge is now serving the prior hashed assets and that index.html references the correct filenames. Run all three checks before closing the incident.

# 1. Confirm the prior hashed JS is served with correct headers
curl -sI "https://cdn.example.com/assets/index-a1b2c3d4.js" \
  | grep -iE "^(http|content-length|cache-control|cf-cache-status|age):"

# 2. Confirm the bad deploy's hash is NOT referenced in the live HTML
curl -s "https://example.com/" | grep -oE '[a-f0-9]{8}\.js' | sort -u

# 3. Confirm the prior hash IS referenced in the live HTML
curl -s "https://example.com/" | grep "index-a1b2c3d4.js"

Expected output for check 1:

HTTP/2 200
content-length: 45678
cache-control: public, max-age=31536000, immutable
cf-cache-status: HIT

If cf-cache-status is MISS immediately after the purge, that is expected — the edge is pulling from origin. Repeat the request: subsequent responses should be HIT. If content-length does not match the bytes value from the prior metafile, the wrong artifact version was uploaded.

What if the prior hashed asset returns a 404?

The artifact was not uploaded successfully, or the S3/origin path does not match the output path in the metafile. Re-run the sync command and confirm the destination key prefix matches the path esbuild wrote — the metafile key dist/index-a1b2c3d4.js becomes assets/index-a1b2c3d4.js on S3 only if your sync strips the dist/ prefix.

What if the CDN still serves the bad version after purging HTML?

Some CDNs have regional propagation lag. Wait 30–60 seconds and retry from a different geographic location using a VPN or a remote curl request. If the bad version persists beyond 5 minutes after a confirmed purge, check that the purge targeted the exact URL including protocol and subdomain — https://example.com/ not http://example.com/ or https://www.example.com/.

When to Reconsider: When a Full Rebuild Beats Redeploying Prior Artifacts

Redeploying prior artifacts is fast and low-risk when the artifacts themselves are intact and were produced by a deterministic build. There are four situations where a full rebuild from the prior commit is the better choice:

  1. Non-deterministic build outputs. If your build injects a timestamp, reads environment-specific secrets into the bundle, or has non-deterministic chunk splitting, rebuilding from the same commit will produce different hashes than the original deploy. In this case, neither approach restores the original filenames — you need to fix the non-determinism first. See the guide on deterministic build outputs.

  2. Corrupted artifacts. If the artifact store itself is suspect — an incomplete upload, a storage failure, or accidental mutation — do not redeploy from it. Rebuild from the commit.

  3. The bad deploy only touched source code, not build config. If a developer pushed broken application logic but the build pipeline itself is fine, rebuilding from the prior commit (before the bad change) produces clean artifacts. This is conceptually the same path as redeploying artifacts but does not require a prior artifact store.

  4. Security incident. If the bad deploy included a compromised dependency or a supply-chain artifact, prior artifacts from the same period may also be compromised. Rebuild from a clean environment using pinned, audited dependencies.

Frequently Asked Questions

Do I need to purge the CDN for every hashed asset when rolling back?

No. Hashed assets are immutable by design — their URL encodes their content. If the prior asset index-a1b2c3d4.js was previously cached by the CDN, that cached copy is correct. You only need to purge the HTML entry point (index.html or equivalent) so the CDN pulls the updated HTML that references the prior hashes.

What if my esbuild build does not write a metafile?

Add metafile: true to your build config immediately and archive the output. Without it, the only source of truth for which hash corresponds to which source file is the filenames themselves — which you can grep from a preserved dist/ snapshot. Going forward, treat the metafile as a first-class deploy artifact alongside the compiled files.

Can I use the bytes field in the metafile to verify artifact integrity?

Yes. The bytes field in each output entry records the exact file size esbuild produced. After redeploying, compare the actual file sizes against the metafile values. A mismatch indicates corruption, a partial upload, or an encoding difference (e.g., the file was re-compressed in transit). It is not a cryptographic check, but it catches the most common artifact integrity failures quickly.

How long should I keep prior build artifacts?

Retain at least the three prior successful builds. In most CI/CD systems this means configuring artifact retention to keep the last 5–10 runs. The cost is minimal — a typical frontend bundle with sourcemaps is under 10 MB compressed — and it eliminates the need for a full rebuild during the most time-sensitive part of an incident.