Recovering From a Deploy That Deleted Live Assets

At 14:02 a routine deploy finished. By 14:09, the error tracker showed a spike of ChunkLoadError and failed stylesheet loads, and two support messages arrived: "your docs look broken — no styling, menus don't work." The pages being served were fine. The problem was readers who already had pages: their HTML, cached at the CDN and open in browser tabs, referenced CSS and JavaScript files from the previous build — and the deploy had deleted them.

This is a write-up of that incident on a documentation site deployed to S3 behind CloudFront with aws s3 sync --delete: how it was detected, how it was recovered in 11 minutes, and the changes that make it impossible to repeat. It belongs to Rollbacks and Deploy Safety for Static Sites; the preventive pattern is covered in Keeping Old Assets Alive After Deploy.

What Happened

Sequence of the incident from deploy to broken page Four actors, the deploy job, S3, the CDN and a reader. The deploy job syncs the new build with the delete flag, removing the previous build's hashed assets from S3. The CDN still holds the previous HTML for up to one hour. A reader requests a page and receives the cached old HTML. The reader's browser requests the old stylesheet, the CDN misses and asks S3, and S3 returns 404. The page renders unstyled. How cached HTML met a deleted asset Deploy job S3 CDN Reader sync --delete: old _astro/* removed GET /docs/install/ cached old HTML (age 40 min) GET /_astro/index.4f8c1b.css miss → 404
HTML was cached for an hour at the edge; the assets it referenced were gone in seconds.

The site's HTML was cached at CloudFront with s-maxage=3600, and invalidated after each deploy — but the invalidation ran after the sync, and took 90 seconds to propagate. Meanwhile, and for HTML pages not in the invalidation list (only changed pages were invalidated), the CDN kept serving old HTML for up to an hour. That HTML pointed at /_astro/index.4f8c1b.css and friends. aws s3 sync --delete had removed them, because the new build had index.9d2e07.css instead.

This had been true of every deploy for months. It became an incident on this deploy because the build tool's upgrade had changed the hash of every asset at once, so every page with cached HTML broke instead of a few.

Detection

SignalTime after deploySource
Asset 404 rate at CDN+2 min (from 0.01% to 6%)CDN real-time metrics
ChunkLoadError in error tracker+4 minRUM error tracking
First support message+7 minHelp desk
Alert fired+9 minError-rate alert

The alert on JavaScript errors fired; there was no alert on asset 404s, which would have fired seven minutes earlier. Real-User Error Tracking on Static Sites covers the tracking that caught it.

Recovery

The on-call engineer's steps, in order:

  1. Restore the previous assets (minute 9–13). S3 versioning was enabled on the bucket, so the deleted objects existed as noncurrent versions. A script listed delete markers created by the deploy and removed them, restoring 412 objects:
aws s3api list-object-versions --bucket "$BUCKET" --prefix _astro/ \
  --query 'DeleteMarkers[?IsLatest && LastModified>=`2026-06-10T14:00`].[Key,VersionId]' --output text |
while read key vid; do aws s3api delete-object --bucket "$BUCKET" --key "$key" --version-id "$vid"; done
  1. Invalidate all HTML (minute 13–15) with /*.html and /*/ patterns so readers stopped receiving old HTML as soon as possible.
  2. Verify (minute 15–20): the CDN's 404 rate for /_astro/ returned to baseline, and the error tracker's ChunkLoadError rate fell to zero.

Total time from alert to resolution: 11 minutes. Without bucket versioning, the fallback would have been checking out the previous commit, rebuilding and uploading its assets — about 6 minutes longer.

Timeline of the incident from deploy to recovery Six events. At 14:02 the deploy finished. At 14:04 asset 404s rose to 6 percent. At 14:11 the error-rate alert fired. At 14:15 deleted assets were restored from S3 versions. At 14:17 all HTML was invalidated. At 14:22 errors returned to baseline. The incident timeline Deploy done 404s at 6% Alert fires Assets restored HTML invalidated Resolved 14:02 sync --delete 14:04 no alert on this 14:11 JS error rate 14:15 delete markers removed 14:17 all pages 14:22 errors at baseline
Readers were affected for 20 minutes; most of the delay was detection.

Impact

About 3,900 page views over 20 minutes received broken pages — 18% of views in that window. No data was lost; the cost was a degraded experience and support time.

Prevention

Four changes, each addressing a different part of the chain:

1. Never delete hashed assets on deploy. The sync for _astro/ no longer uses --delete. A weekly job deletes hashed assets that are not referenced by any of the last five builds' manifests and are older than 7 days. Storage cost of keeping them: about 40 MB, negligible.

aws s3 sync dist/_astro "s3://$BUCKET/_astro" --cache-control 'public, max-age=31536000, immutable'   # no --delete
aws s3 sync dist "s3://$BUCKET" --exclude '_astro/*' --delete --cache-control 'public, max-age=0, s-maxage=3600'

2. Upload assets before HTML. New assets go up first, then HTML, so there is never a moment where new HTML references assets that have not arrived.

3. Alert on asset 404s. A CloudFront metric alarm on the 4xx rate for /_astro/* fires above 0.5% for two minutes — it would have caught this in two minutes rather than nine.

4. Keep versioning on. Bucket versioning with a 30-day noncurrent-version expiry stays enabled, because it made recovery a script instead of a rebuild.

Safe order of operations for deploying to S3 and CloudFront Five steps. Upload new hashed assets without deleting old ones. Upload HTML and other files, deleting removed pages. Invalidate changed HTML paths. Verify a sample of pages and their assets return 200. A weekly job prunes hashed assets unused by the last five builds and older than seven days. The deploy order now Upload new assets no --delete Upload HTML --delete for pages Invalidate HTML changed paths Verify assets 200 sample pages Weekly prune unused, > 7 days
Old assets now outlive any cached HTML that could reference them.

Moving to atomic deploys with versioned prefixes, described in Rolling Back S3 and CloudFront Deploys With Versioned Prefixes, removes this class of problem entirely and was done the following quarter.

What the Post-Incident Review Changed Beyond the Deploy

The review looked past the deploy script at why the problem had existed unnoticed for months. Three findings led to changes outside the pipeline itself.

First, the deploy script had been copied from a blog post when the site was small and deployed weekly. Nobody had revisited it as the site grew to several deploys a day, which multiplied the number of windows in which cached HTML could outlive its assets. Deploy scripts now have an owner listed in the repository's CODEOWNERS file, and changes to them need that owner's review.

Second, small numbers of asset 404s had shown up in the CDN logs after most deploys for months — 0.1% to 0.3% for a few minutes — and had been dismissed as noise from bots. They were readers with open tabs. The new alarm's threshold was set below that level so the pattern would have been visible.

Third, the runbook for "site looks broken" had started with "check whether the latest deploy succeeded", which it had. The runbook now starts with the asset 404 rate and the error tracker, and includes the version-restore script above, tested in a quarterly drill against a staging bucket. The first drill found that the script's date filter used the wrong time zone, which would have restored nothing — exactly the kind of mistake better found in a drill than during an incident.

Communicating During the Incident

A short status note went on the site's status page at 14:13, two minutes after the alert, saying that some readers might see unstyled pages and that a fix was in progress, and was resolved at 14:24. Support used a canned reply asking affected readers to reload, which fixed it for anyone whose HTML had already been refreshed at the edge.

Measured Impact of the Changes

MeasureBeforeAfterSource
Asset 404 rate in the hour after deploysup to 6%<0.01%CloudFront metrics
Time to detect asset 404 spikes9 min (via JS errors)2 min (direct alarm)Alarm config
Recovery pathAd-hoc scriptDocumented runbook + versioningRunbook
Extra storage for retained assets—~40 MBS3 metrics

Pitfalls & Rollback

  • --delete on hashed asset folders. Old HTML still references them.
  • Uploading HTML before assets. New pages briefly reference missing files.
  • Alerting only on JavaScript errors. Alert on asset 404s directly.
  • No versioning on the bucket. It turns recovery into a rebuild.
  • Rollback: restoring deleted objects from versions is the rollback; the runbook script above does it in minutes.

Conclusion

Deleting the previous build's assets during a deploy breaks every page still served from cached HTML. On this site an asset hash change made it an incident affecting 18% of views for 20 minutes; bucket versioning made recovery an 11-minute script. Keeping old hashed assets for a week, uploading assets before HTML, alerting on asset 404s and later moving to versioned prefixes mean it cannot happen again.

FAQ

Why does deleting old assets on deploy break pages?

HTML is cached for some time in CDNs and browsers, and tabs stay open for hours. That cached HTML references the previous build's hashed CSS and JavaScript files. If the deploy deletes those files, any reader with old HTML requests assets that no longer exist, gets 404s, and sees an unstyled or broken page until their HTML refreshes.

How long should old assets be kept after a deploy?

At least as long as the longest time old HTML can be served or stay open, which on most static sites means the CDN's HTML TTL plus a margin for open tabs. Keeping the previous few builds' assets for seven days covers almost every case at negligible storage cost, because hashed assets are small and change rarely.

How can I recover quickly if assets were deleted?

Redeploy the previous build's assets alongside the current build, or restore them from object storage versioning if it was enabled. Purging HTML from the CDN shortens the window during which readers request old assets. Atomic-deploy hosts avoid the problem because previous deployments stay available.

Do atomic deploy hosts have this problem?

Less so. Hosts such as Netlify, Vercel and Cloudflare Pages keep every deployment's files, and many serve assets from previous deployments for some time. The problem appears mostly with self-managed deploys to object storage or servers using sync tools that delete files missing from the new build.

Filed underFixes