You shipped a deploy. Minutes later your error tracker lights up:
ChunkLoadError: Loading chunk 5760 failed.
(error: https://yourapp.com/_next/static/chunks/5760-40c839657ea31a15.js)
Or the timeout variant:
ChunkLoadError: Loading chunk app/layout failed.
(timeout: https://yourapp.com/_next/static/chunks/app/layout.js)
The instinct is to wrap something in a try/catch. Don't — this is almost never a code bug. It is version skew: a browser is asking for a JavaScript chunk that your latest deploy already deleted. This guide explains the real causes and the official Next.js configuration that fixes each one.
{ name: "Next.js", version: "13.4–16" },
{ name: "App Router", version: "& Pages Router" },
{ name: "webpack", version: "chunk loading" },
]} />
What the two variants actually mean
The parenthetical in the message tells you which failure mode you hit:
-
(error: <url>)— the request for the chunk completed but failed, typically a 404. The file no longer exists at that hashed path. -
(timeout: <url>)— the request never completed within webpack's chunk-load timeout (default 120000 ms, set viaoutput.chunkLoadTimeout). This is the network/slow-connection branch.
Knowing which one you have points you at the right fix below.
Root cause #1: stale chunks after a new deploy (the common one)
Next.js fingerprints static assets with a content hash in the filename (5760-40c839657ea31a15.js). When you deploy, the bundle changes, the hashes change, and the old files are gone. Any client still running the previous version — an open tab, a bfcache restore, a CDN serving stale HTML — requests the old chunk path and gets a 404.
Next.js documents this exact failure mode under self-hosting and calls it version skew: "Missing assets: The client requests JavaScript or CSS files that no longer exist on the server." Rolling deployments make it worse, because some instances serve new assets while others still serve old ones.
Give every build a stable deployment identifier. Next.js appends it (?dpl=…) to asset URLs and exposes it via response headers; when the client detects a mismatch between its deployment ID and the server's, it triggers a hard navigation (full page reload) instead of a broken client-side navigation.
// next.config.js
module.exports = {
deploymentId: process.env.DEPLOYMENT_VERSION || process.env.GIT_SHA,
}
This is the official, intended mechanism for exactly this cause. See the deploymentId config and self-hosting version skew docs.
deploymentId shipped as experimental.deploymentId in v13.4.10 and was promoted to a top-level config option in v14.1.4. Before 14.1.4, use the experimental. form.
Root cause #2: rolling deploys with mismatched build IDs
If you run multiple containers/instances behind a load balancer, each one generates its own build ID by default, so they disagree on chunk names during a rollout. Pin the build ID so every instance agrees:
// next.config.js
module.exports = {
generateBuildId: async () => process.env.GIT_HASH,
}
Documented under generateBuildId.
Root cause #3: self-hosted output: 'standalone' builds
If you self-host with output: 'standalone' (common in Docker), the minimal server does not copy public or .next/static by default. Every chunk then 404s until you copy them. Straight from the output docs:
cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
Root cause #4: a CDN or proxy serving stale HTML
Next.js sets Cache-Control: public, max-age=31536000, immutable on hashed static assets — they are safe to cache forever because the hash changes when the content does. The problem appears when a CDN, reverse proxy, or service worker caches the HTML (which references the hashed chunks) past its life, or rewrites/misroutes /_next/static/ requests.
wrong="CDN caches the HTML document for hours, so it keeps pointing browsers at chunk hashes from a deploy you already replaced."
right="Let the immutable headers on /_next/static stand, keep HTML short-lived, and never strip or redirect /_next/static/ at the proxy. Use assetPrefix if assets live on a separate domain."
/>
If you serve assets from a separate domain/CDN, configure assetPrefix so chunk URLs are generated correctly.
Root cause #5: network failure / timeout (the (timeout:) variant)
A flaky connection, an aggressive ad-blocker, or a slow CDN can make the chunk request hang past webpack's limit. That surfaces as the (timeout:) form. The relevant knob is webpack's output.chunkLoadTimeout (default 120000 ms), reachable through a webpack override in next.config. Treat this as a webpack-level setting, not a Next.js flag.
A safety net for dynamic imports
When you load a component with next/dynamic or React.lazy, React's docs are explicit: "If the Promise rejects, React will throw the rejection reason for the nearest Error Boundary to handle." So a failed chunk load is catchable by an error boundary — you can render a "please refresh" fallback.
The error-boundary catch is documented (see React lazy). The widely-shared "catch ChunkLoadError → reload once via sessionStorage guard" pattern is a community technique, not official guidance. It is a reasonable last-resort UX layer, but it does not address the root cause — ship deploymentId first.
- The chunk genuinely fails to parse in an old browser (a real SyntaxError, not version skew) — that is a code/transpile-target issue, not a deploy issue.
- You only see it locally in dev — clear
.nextand restart; dev chunking differs from production. - Your
import()path is actually wrong or the module throws at module scope — fix the import, an error boundary only masks it.
Decision guide
| Symptom | Most likely cause | First fix |
|---|---|---|
| Spikes right after each deploy | Version skew (#1) | deploymentId |
| Only on multi-instance/rolling deploys | Mismatched build IDs (#2) | generateBuildId |
| Self-hosted Docker, 404 on every chunk | Standalone missing assets (#3) | copy .next/static + public
|
| Behind Cloudflare/proxy, intermittent | Stale HTML cache (#4) | fix CDN/HTML cache headers |
(timeout:) in the message |
Network/slow load (#5) | check connectivity / chunkLoadTimeout |
Related Articles
- Fix Next.js Build Error Module Not Found After Deploy
- Deploy Next.js 15 to Vercel Without Environment Variable Errors
- Next.js Turbopack Stuck on Compiling: How to Fix
- Deploying Next.js + Supabase to Production
Frequently Asked Questions
Why do I get ChunkLoadError only after deploying?
Because a user had a tab open (or a CDN served cached HTML) referencing the old hashed chunk filenames. Your new deploy replaced those files with new hashes, so the old chunk URL now 404s. Next.js calls this version skew — set deploymentId so Next forces a full reload on mismatch.
Is ChunkLoadError a bug in my code?
Usually not. It is a deployment/caching class of problem, which is why upgrading Next.js rarely fixes it. The fix is configuration — deploymentId, generateBuildId, correct CDN headers.
Does a try/catch around import() fix it?
A React error boundary catches the rejected dynamic import so you can show fallback UI. The "reload once" pattern on top is a community technique, not official — use it as a safety net, not the fix.
Originally published at https://www.iloveblogs.blog
Top comments (0)