Next.js Build Cache Hit Our Production Deploy at 3 AM
A truncated prerendered shell from a restored Turbopack cache served a blank page in production. Here is the fix we kept.
Author
Next.js Build Cache Hit Our Production Deploy at 3 AM
We were asleep when the pager lit up. A hard refresh of our client site's home route returned a blank page. The deploy log said success. The response headers said x-vercel-cache: HIT. The HTML body was 64KB of bootstrap markup and a bare multipart boundary, with zero self.__next_f.push rows and no closing </html> tag.
This is the field note we now read to every intern before they touch a production deploy.
The Incident: Blank Page at 3 AM
At 02:58 IST, our monitoring caught a spike in client-side errors on /. A hard load returned HTTP 200 with content-type: text/html and x-vercel-cache: HIT, but the body was a truncated prerendered shell. No inlined RSC payload, no closing </html>, just a bare multipart boundary (-- --) ending the document mid-stream.
Hydration aborted immediately. The page stayed blank. Meanwhile, every _next/static/chunks/*.js asset referenced by the broken shell returned 200. Only the build-time static prerender was corrupt.
Root Cause: Turbopack Build Cache Corruption
The broken deploy built on a restored cross-deploy Turbopack build cache. The build log line read Restored build cache from previous deployment (). The incremental turbopackFileSystemCacheForBuild path emitted a truncated prerendered shell for /, reporting ◐ / Partial Prerender as success.
A from-scratch rebuild of the same source produced a healthy 306KB shell with 63 self.__next_f.push([1,"…"]) rows ending in a proper </html> tag. The only variable between the two builds was the restored cache.
What We Tried First (And Why It Failed)
Attempt 1: Rolling Back the Deploy
We rolled back to the previous production deploy. The blank page persisted. Both deploys coexisted with the same truncated shell. Rolling back did not clear the corrupted cache entry that was being served.
Attempt 2: Clearing Vercel's Edge Cache
We purged the Vercel edge cache for the route. The next hard load still returned the truncated 64KB shell. The corruption lived in the build output, not the edge cache. x-vercel-cache: HIT was misleading because the edge was faithfully serving a broken origin artifact.
Attempt 3: Rebuilding Without Cache
We triggered a fresh build with cache cleared. The build succeeded and emitted the healthy 306KB shell. This confirmed the restored build cache was the root cause, but it was a manual fix, not a systemic one.
The Working Fix: Disable Build Caches in Production
We adopted the approach from the Next.js core team commit 94e9fa6, which disables build caches for production, staging, and force-preview deploys.
Setting NEXT_SKIP_BUILD_CACHE=1 in CI
We added a job-level environment variable to our CI pipeline for all production and staging deploy targets:
env:
NEXT_SKIP_BUILD_CACHE: 1
This causes every caching step to skip:
- Rust cache (
ijjk/rust-cache), skipped - Native binary cache (
native-cache.jsrestore/save), skipped - Turbo remote cache, set to
localfor non-Docker builds, not passed to Docker - sccache, env vars not passed to Docker builds
Automated-preview builds (PRs) continue to use all caches as before, keeping iteration fast for development.
Configuring Turbopack Cache Keys with content-hash
For our monorepo builds, we also pinned Turbopack to 2.0.4+ and enabled content-hash based cache keys, following the guidance from the johal.in war story:
// next.config.js
module.exports = {
experimental: {
cacheKeyStrategy: 'content-hash',
},
};
This eliminated the mtime-based cache key issues that break for symlinked workspace dependencies in monorepos.
Verifying Healthy Shell Output (306KB vs 64KB)
We added a post-build validation step to our CI pipeline that checks the prerendered shell size and structure:
# Validate prerendered shell integrity
SHELL_SIZE=$(wc -c < .next/server/app/page/page.html)
if [ "$SHELL_SIZE" -lt 100000 ]; then
echo "ERROR: Prerendered shell is suspiciously small ($SHELL_SIZE bytes)"
exit 1
fi
# Check for closing html tag
if ! grep -q '</html>' .next/server/app/page/page.html; then
echo "ERROR: Prerendered shell missing closing </html> tag"
exit 1
fi
# Check for RSC payload pushes
PUSH_COUNT=$(grep -c 'self.__next_f.push' .next/server/app/page/page.html)
if [ "$PUSH_COUNT" -lt 10 ]; then
echo "ERROR: Prerendered shell has only $PUSH_COUNT RSC pushes"
exit 1
fi
echo "Prerendered shell OK: $SHELL_SIZE bytes, $PUSH_COUNT RSC pushes"
Pitfalls We Warn Interns About
Never Trust Restored Build Caches in Production
A restored build cache can silently produce corrupt output that passes all build checks. We now treat any restored cache in a production deploy as a risk to be mitigated, not an optimization to be trusted.
The Danger of Silent Build Success with Corrupt Output
The build reported success with ◐ / Partial Prerender. The corruption was only visible at runtime. This is why we added build output validation to CI, a successful build does not guarantee correct output.
Why x-vercel-cache: HIT Can Be Misleading
HIT means the edge served a cached response. It does not mean the cached response is correct. When the origin artifact is corrupt, the edge faithfully serves the corruption.
What We Would Do Differently Next Time
Enforce Cache-Free Production Deploys by Default
We are making NEXT_SKIP_BUILD_CACHE=1 the default for all production and staging environments, not an override. This removes the human decision point where someone might skip it.
Add Build Output Validation to CI Pipeline
The validation script above is now a required step in every production deploy. If the prerendered shell is truncated, missing closing tags, or has too few RSC pushes, the deploy fails before it reaches production.
Monitor Prerender Shell Integrity in Staging
We are adding synthetic monitoring that hard-loads key routes in staging and validates the HTML structure, catching truncation before it reaches production.
Sources
Sources
Related reading
Enjoyed this article?
Back to Blog


