How a 1.337% Random Check Permanently Broke Our Next.js Site
OpenChaos is a repo where anyone submits a PR, the community votes with GitHub reactions, and the most-voted PR merges daily. Last week, this joke PR won the vote:
export default function Home() {
if (Math.random() <= 0.01337) {
return null;
}
// ... rest of the page
}
A 1.337% chance to return nothing. A leet joke. It won the popular vote, merged to production, and permanently took the site down.
Not for 1.337% of visitors. For everyone. Indefinitely.
What You'd Expect
The code looks harmless. Math.random() rolls. 98.663% of the time, the page renders normally. One in seventy-five visits, a visitor gets a blank screen. Funny.
But page.tsx is a Next.js Server Component. There's no "use client" directive. This code runs on the server, not in the browser.
Still — you'd expect the server to re-render the page periodically. Our data-fetching layer uses Incremental Static Regeneration:
// src/lib/github.ts — used by PRList, HallOfChaos, etc.
const response = await fetch(url, {
headers: getHeaders("application/vnd.github.v3+json"),
next: { revalidate: 300 }, // Re-render every 5 minutes
});
Five fetch() calls, each with revalidate: 300. The page should refresh every five minutes. Even if Math.random() hits and the page goes blank, it should self-heal on the next revalidation cycle. A five-minute blip at worst.
That's not what happened.
What Actually Happened
When Math.random() hit and the page returned null, the child components — PRList, HallOfChaos, everything below the early return — never rendered. React short-circuits: if the parent returns null, children don't execute.
Those child components are where all five fetch() calls live. The calls that set revalidate: 300. The calls that tell Next.js "re-render this page in five minutes."
None of them executed.
With no revalidate hint in the render output, Next.js treated the blank page as fully static content. It cached the empty response with no expiration. The blank page became the permanent, canonical version of the site — served to every visitor until the next deployment.
The revalidation mechanism that would have self-healed the page was killed by the very bug it needed to recover from.
The Causal Chain
Math.random() hits 1.337%
→ page.tsx returns null
→ child components don't render
→ fetch() calls don't execute
→ no revalidate hint is set
→ Next.js caches blank page as fully static
→ permanent outage until next deploy
One roll. One cache entry. Site down for everyone.
Why This Happens
In Next.js App Router with ISR, revalidation is not a page-level configuration. It's driven by the fetch() calls that execute during render. Each fetch() with next: { revalidate: N } registers a revalidation window. The shortest one wins — that's what determines when the page re-renders.
If your render doesn't execute any revalidating fetches, there's nothing to schedule a re-render. The page is treated as static. This is documented behavior, but the implication for early returns isn't obvious.
The general footgun: any early return in a Server Component that skips all fetch() calls will produce a page with no revalidation. If that page gets cached, it's cached forever.
This isn't specific to Math.random(). Any conditional early return will do it:
// All of these can poison the ISR cache:
if (isMaintenanceMode()) return <MaintenancePage />;
// ^ If MaintenancePage has no revalidating fetches
if (!featureFlag) return null;
// ^ Cached permanently if flag is false at render time
if (error) return <ErrorPage error={error} />;
// ^ Your error page might be cached forever
The Fix
Move the stochastic behavior from the server to the client:
"use client";
export function IE6Layout({ children }: IE6LayoutProps) {
const [cursed, setCursed] = useState(false);
useEffect(() => {
if (Math.random() <= 0.01337) {
setCursed(true);
}
}, []);
if (cursed) return null;
// ...
}
The server always renders real content (with all its fetch() calls and revalidate hints intact). The 1.337% check runs per-visitor in the browser after hydration. The joke survives. The cache doesn't get poisoned.
The more general fix: never put an early return before your revalidating fetches in a Server Component, or explicitly set revalidate at the segment config level:
// page.tsx
export const revalidate = 300; // Segment-level fallback
export default function Home() {
if (unluckyCondition) return null;
// Page still revalidates even on the empty path
}
The Broader Pattern
This is a specific instance of a general class of bugs: recovery mechanisms that only exist on the happy path.
- HTTP error pages that don't set
Cache-Controlheaders → CDN caches the error permanently - Database connections released only in the success branch → pool leaks on errors
- Health checks that depend on the service being healthy to report unhealthy
- Circuit breakers that can't trip because the monitoring runs inside the circuit
The pattern: some piece of infrastructure (cache invalidation, connection cleanup, health reporting) only runs when things are working. The moment things break, the infrastructure that would fix them breaks too.
In our case, cache revalidation only happens when the page renders fully. The moment the page doesn't render, revalidation stops, and the broken state becomes permanent.
Epilogue
The bug was diagnosed by @bigintersmind, who submitted the fix. CI passes. No conflicts. Ready to merge.
It waited in the vote queue behind a DOOM port. Then the DOOM port merged — with the fix baked in. Trojan horse. Site back up.
Follow the chaos
Weekly stories from a repo where the internet decides what ships. No spam, just drama.