ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-14Advanced

When the Edge Cache Pinned Next.js Error Pages: A cache-worker Guard Design

Users reported intermittent 'failed to load' errors I could never reproduce. The cause: SSR exceptions shipped as HTTP 200 and pinned by the edge cache. Here is how I narrowed it down with an Antigravity agent and added a cache-worker guard to stop it.

antigravity346cloudflare6nextjs4edge-cache2ssr

Premium Article

"Once in a while the site shows a 'failed to load' error, but a reload fixes it." Running a set of technical blogs as an indie developer, I started getting reports like this. The frustrating part was that I could never reproduce it. Dozens of reloads, always fine. My logs showed almost no 5xx responses.

The cause, as it turned out, was this: Next.js was shipping an exception thrown mid-SSR as an HTTP 200, and Cloudflare's edge cache was pinning that broken response for hours. Because it returns 200, monitoring never flags it; because it gets cached, only a subset of visitors keeps hitting the broken page. That combination is genuinely hard to observe. Below is how I traced it and the cache-worker guard I added so it would not happen again.

The blind spot: an error page that returns 200

App Router streams the body. The status code is flushed before the body, so once 200 OK has gone out, an exception thrown later during React rendering can no longer rewrite the status. Instead, the error.tsx UI is streamed as a continuation of the same body.

So from the visitor's side it is a "failed to load" screen, while the HTTP status is 200 OK. That was the first blind spot. A dashboard that counts 5xx sees nothing wrong.

There was a second path too. Article bodies load static HTML through getCloudflareContext().env.ASSETS.fetch(). During the brief moment a deploy flips over, that ASSETS read can come up empty. No exception is thrown, but the article page renders with an empty body and still returns 200. The error screen and the empty-body page look different, but they fall into the same hole: a "broken 200."

Why the edge cache made it worse

Layer the Cloudflare Workers cache-worker on top and the problem amplifies. The cache-worker at the time was naive — it cached any 200 HTML to the edge for four hours, essentially unconditionally.

// The naive version that caused the problem
const res = await fetch(request);
if (res.status === 200 && isHTML(res)) {
  const toCache = res.clone();
  ctx.waitUntil(cache.put(request, toCache)); // stored without inspecting the body
}
return res;

Deploy flips happen a dozen-plus times a day. If a "broken 200" generated in that window happens to land in the cache, then for the next four hours only visitors routed to that edge location keep getting the broken page. The reason it never reproduced for me was simple: I was being served a healthy cache from a different location. The "a reload fixes it" reports also line up once you account for cache variance and deploy intervals.

The docs say "200 is cacheable," but under streaming SSR, "status 200" and "the body is intact" are two different things. Conflating them was the design mistake.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
If users report intermittent load errors you can never reproduce, you'll understand how streaming SSR exceptions ship as HTTP 200 and pinpoint the root cause
You'll be able to write a ~30-line cache-worker guard that refuses to store pages containing an error marker, a missing </html>, or an empty content container
You'll learn the design tradeoffs for suppressing empty pages during deploy transitions using a single ASSETS retry and a no-store header on 5xx, ready to apply to your own Cloudflare Workers project
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-05-02
Stopping Server Action Foot-Guns Before Antigravity Ships Them — Zod, revalidate, and Authorization Built In From the First Draft
Ask Antigravity to write a Next.js Server Action and you'll often get back code with no Zod parsing, no revalidate call, and no authorization check. Here's how to bake those in from the start instead of patching them on later.
App Dev2026-04-30
Antigravity × Better Auth: Building a Modern TypeScript Auth Stack End-to-End
End the NextAuth fatigue with Better Auth. A practical Antigravity-driven guide that ships schema generation, OAuth, RBAC, and Passkeys with production-ready patterns.
App Dev2026-04-29
Antigravity × Cloudflare Vectorize: Build a Production RAG Pipeline That Runs at the Edge
A production guide to building a RAG pipeline on Cloudflare Vectorize using Antigravity. Chunking, hybrid search, cost design, and observability—covered at a depth a solo developer can actually ship.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →