Fix ChunkLoadError: Loading chunk failed (Next.js, webpack)
"ChunkLoadError: Loading chunk 5760 failed" almost always means a user has an old tab open after you shipped a new deploy. The fix is configuration — deploymentId, build IDs, and CDN headers — not a try/catch.
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)Those two are webpack's wording. A Next.js 16 app built with Turbopack, the default bundler, reports the same failure differently. We reproduced it on 1 October 2026 (Next.js 16.3.8, next start, headless Chromium): load the page, delete the chunk files it has not fetched yet to simulate a newer deploy, then trigger a next/dynamic import:
Turbopack build: ChunkLoadError | Failed to load chunk /_next/static/chunks/1fxg5mnf5zpy6.js from module 89073
webpack build: ChunkLoadError | Loading chunk 9 failed.
(error: http://localhost:3010/_next/static/chunks/9.bc01776b4de22c3d.js)The error name is ChunkLoadError in both cases, so a filter on the name catches both; a filter on the "Loading chunk" text misses every Turbopack build.
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.
What the two webpack variants actually mean#
In a webpack build, the parenthetical in the message tells you which failure mode you hit (the Turbopack message above has no such suffix):
(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 this cause, with one limit the docs spell out: the check runs on navigation requests, and "A per-deployment value only avoids skew if requests are also routed by deployment. Next.js does not route on ?dpl=, so that routing comes from your host or CDN. Without it, clients that reach an instance from another deployment during a rollout will reload rather than navigate." 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: each container runs its own build#
Chunk file names come from content hashes, not from the build ID, but the build ID is how Next.js identifies which version is being served. The generateBuildId docs say: "The same build should be used and boot up multiple containers." Build once and ship that output to every instance. If you have to rebuild for each stage, pin the build ID:
// next.config.js
module.exports = {
generateBuildId: async () => process.env.GIT_HASH,
}Do not combine it with deploymentId expecting both to work: the same page notes that "When deploymentId is set, Next.js uses a constant build ID and generateBuildId has no effect." (the deploymentId version history dates that behaviour to v16.2.0).
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.
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, and it only applies to next build --webpack: Turbopack does not read a webpack() function, and in Next.js 16 a plain next build fails on purpose while one is present (see the version 16 upgrade guide).
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.
Check your own headers#
Root causes #1 and #4 leave a trace in the HTTP headers: the HTML of the page and the chunk it asks for. Run curl -I on both — the page URL, then one /_next/static/chunks/... file from the failing request — and paste the output below. The checker reads the caching and status headers locally in your browser (nothing is sent anywhere) and tells you whether stale HTML, a CDN cache hit or a rewrite is behind the error, with the fix for your host.
Runs entirely in your browser — nothing you paste is sent anywhere. Cookie values, Authorization and token-like URL parameters (query and fragment) are masked in the output.
curl -sI https://your-site.com/some-page
curl -sI https://your-site.com/_next/static/chunks/…js
Take the chunk URL from the error itself (Loading chunk 5760 failed. (error: https://…)) or from DevTools → Network, filtered on _next/static. curl -v and curl -L output work too.
Decision guide#
| Symptom | Most likely cause | First fix |
|---|---|---|
| Spikes right after each deploy | Version skew (#1) | deploymentId |
| Only on multi-instance/rolling deploys | Each container built separately (#2) | one build for every instance |
| 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 |
Further reading: Fix error:0308010C digital envelope routines::unsupported. Also see Fix UnhandledSchemeError: Reading from "node:buffer", Fix Failed to fetch dynamically imported module (Vite).
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix UnhandledSchemeError: Reading from "node:buffer"
Reproduced on Next.js 16.4.0 with webpack: which import triggers the error, why bare "buffer" builds but "node:buffer" does not, and the three fixes that compile.
Fix Failed to fetch dynamically imported module (Vite)
Reproduce the production-only Vite error in Chromium, see why retrying the same import does nothing, and ship a reload handler that cannot loop.
Fix Next.js: Unable to Acquire the Dev Server Lock
Find the server that owns .next/dev/lock, distinguish a held lock from a leftover file, and restart without disrupting other Node applications.


