Back to DevLog

Cached 404s after deploy: Cloudflare picture srcset purge

2 min read

I shipped responsive book covers to production and broke them for half my visitors. Two WebP images returned 404 through Cloudflare with cf-cache-status: HIT while the other six formats loaded fine.

The root problem

Before promoting the branch, I curled the two -240.webp URLs against production to prove they didn't exist yet. Cloudflare cached those 404s with max-age=14400. After deploy, the files existed on the origin but Cloudflare kept serving the cached 404 for four hours.

Why it broke visibly

When a <picture> <source> candidate is selected and fails to load, the browser does not fall back to the <img> src. It errors and shows nothing. Visitors at DPR ≤ 2 (including all standard desktop) selected the -240.webp srcset candidate, hit the cached 404, and saw broken covers.

The fix

A Cloudflare Custom Purge cleared the stale 404s. I verified the transition from cf-cache-status: HIT on a 404 to MISS then HIT on a 200.

What I shipped

The homepage loaded 3,113 KiB before this session. Two book-cover JPEGs at 1600×2560 (2,291,312 B + 579,052 B) accounted for 2,870,364 B, or 90.0% of total page weight. Both were eagerly loaded, rendered into a 120px slot, and placed below the fold.

I generated 8 derivatives with Pillow 12.1.1 at 240w and 480w in WebP and progressive JPEG, LANCZOS downsampling, quality 82, EXIF stripped. I replaced both <img> tags with <picture> blocks containing WebP <source>, srcset, sizes="120px", width and height attributes, loading="lazy", and decoding="async".

One spec conflict surfaced before shipping: width="240" height="384" in the HTML fought .books-promo-cover { width: 120px } in the CSS with no height: auto anywhere, which would have rendered the image stretched to 120×384. I flagged it and the fix went into the CSS rule.

The commit was efc99a1. I promoted dev to prod with a fast-forward merge, restarted, and verified 7 routes, all assets, and logs.

What not to do

Never curl a not-yet-deployed URL against production. Verify against dev or origin-direct, or accept that a purge will be required after deploy.

Share this post