Skip to content

Build1 publisher3 min readPublished

Escrozon's pre-launch URL check left Cloudflare serving a 404 for almost an hour

Cloudflare's one-hour HTML cache rule stored the 404 from Escrozon's pre-launch check and kept serving it after the deploy returned 200. Without Tiered Cache each data center keeps its own copy, so one region can see the page while another still gets the error.

The Engineer · Build desk

Illustration accompanying Escrozon's pre-launch URL check left Cloudflare serving a 404 for almost an hour

What happened

  • Escrozon, an escrow marketplace for digital assets behind Cloudflare, shipped a new landing page that deployed cleanly but returned 404 to visitors for almost an hour.
  • Before the deploy, the team requested the new public URL to see whether it was live and got a 404, correct at that moment.
  • The team's Cloudflare cache rule holds HTML at the edge for an hour with s-maxage=3600, and it applied to that 404 as well.
  • The app sent s-maxage=300, but the dashboard rule overrode it at the edge, so edits to the Next.js header config changed nothing.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Edge TTLs have to be split by status code in Cache Rules, with 404s or all 4xx on no cache or a few seconds, so an early request cannot pin an error for an hour.
  • exposure Without Tiered Cache the stale 404 lives per data center, so one region's successful load is no evidence that visitors elsewhere can see the page.
  • cost Purge Everything clears the stale copy but empties the whole cache, so deploy scripts that purge only the shipped URLs avoid refilling every location from origin.
  • constraint Pre-launch checks have to hit localhost or the origin directly, because a single request to the public URL is enough to seed the cached 404.

Once the deploy finished, the origin answered the new URL with a 200 [4]. Cloudflare did not ask it. The edge kept serving its stored 404 until that copy expired [4][6]. The request meant to confirm the page was not live yet went on confirming it for almost an hour after launch [1]. "One curl to the public URL is enough to cache the 404," the Escrozon team wrote [15].

The failure also looks uneven. Unless Tiered Cache is on, each Cloudflare data center keeps its own copy, so visitors in one region may see the page while others still get the 404 [5]. A colleague who loads the page fine from another region has not shown that anyone else can [5].

The post's diagnosis is good, and short. `cf-cache-status: HIT` means Cloudflare answered from its cache, and `age` is how many seconds that copy has been stored [6]. In the team's sample, a 404 at age 2140 under s-maxage=3600 had 1460 seconds left, about 24 minutes [7]. A query string such as `?cb=1` makes Cloudflare treat the request as a different URL [8]. A 200 there and a 404 on the bare URL means the deploy worked and only the cached copy is stale [8].

Precedence caught them twice. The app's own s-maxage=300 lost to the dashboard rule [11]. Browser Cache TTL, a separate Cloudflare setting, can override the max-age the origin sends to browsers. Escrozon's was 4 hours, and returning visitors saw old pages long after a deploy [12]. The sample 404 carries max-age=14400, or 4 hours [6][17]. Setting Browser Cache TTL to "Respect Existing Headers" put the origin back in charge [12].

Of the fixes the post lists, I'd make the per-status TTL first. Cache Rules can set an edge TTL per status code, so 404s, or the whole 4xx range, get no cache or a few seconds while working pages keep the long TTL [10]. That rule covers every URL, including ones a deploy script leaves off its purge list. Purging is the second layer. Custom Purge in the dashboard takes an exact URL, the purge API takes a list of files from a deploy script, and Purge Everything works but empties the whole cache [9]. Without Tiered Cache, every location fetches from the origin separately, and the first visitor in each region pays the full trip [14].

Measuring the cache afterward has its own traps. A cache-busting query string always forces a MISS and adds a full trip to the origin, so timings taken that way look worse than what visitors get [13]. A quick `curl -I` warms the cache, and the next request looks like a HIT [13]. The post's answer is to capture the status code, the timing and `cf-cache-status` from a single request [13].

What to watch

  • Escrozon enabling Tiered Cache would remove the per-data-center copies that made the stale 404 look different from region to region.
  • The 404 edge TTL Escrozon settles on, no cache or a few seconds, both of which its own advice allows.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories