Home → Next.js ChunkLoadError after deploying: why open tabs break and how to fix it (tested on Next.js 16)

Next.js ChunkLoadError after deploying: why open tabs break and how to fix it (tested on Next.js 16)

By · Node.js & JavaScript developer
Published October 20, 2023 · Updated October 1, 2026

Everything works locally and right after the deploy, but your error tracker fills up with this:

ChunkLoadError: Failed to load chunk /_next/static/chunks/03o6ntprxxmm-.js from module 65009

That's the message from Next.js 16 with Turbopack. Webpack builds say ChunkLoadError: Loading chunk 123 failed. The original version of this post suggested deleting cache folders before building, rebuilding, and hard-refreshing the browser. A hard refresh does make the error go away for you, but it does nothing for your users, and the build cache was never the cause.

What actually happens

Next.js splits your client code into chunks named after a hash of their contents, such as 03o6ntprxxmm-.js. Change the code and the name changes. A new deploy usually replaces the old build completely, so the old chunk files are gone.

A browser tab opened before the deploy still runs the old JavaScript. As soon as it needs a chunk it hasn't loaded yet (code behind a dynamic import(), next/dynamic, or a route it hasn't visited), it asks for the old file name and gets a 404. The Next.js docs call this mismatch version skew.

Reproducing it

The test app is a minimal Next.js 16.3.8 App Router project (React 19.3), built and served with next start on node:24-slim. A dashboard loads a "chart" module on demand:

// Stands in for a heavy library that is only loaded on demand
export function drawChart() {
  return `chart from build ${process.env.NEXT_PUBLIC_VERSION}`;
}
<button onClick={async () => {
  const { drawChart } = await import('./chart');
  setText(drawChart());
}}>Show chart</button>

The app was built twice, as v1 and v2. The two builds differ in the version string, so the page chunk and the chart chunk get new names. A Playwright script opened the page in Chromium on v1. A small proxy in front of the servers then switched to v2, which is exactly what a deploy looks like from an open tab: same URL, new server. Then the script used the old tab. Every scenario started with a fresh tab:

Action in the old tabDefault configv2 also serves v1's static files
Show chart (plain import())ChunkLoadError, 404 for 03o6ntprxxmm-.jsworks (the v1 chart)
Show chart (with importOrReload, below)one reload to v2, then worksworks, no reload
Link to a prefetched pageshows the v1 page (prefetched before the deploy)same
Link with prefetch={false}full page reload, shows v2same

So the error is real and easy to trigger: one dynamic import after one deploy. Navigation is less fragile than you might expect, though. When the old client fetched a page from the new server, Next.js noticed the different build and did a full page load instead of a client-side navigation, so the user got a working v2 page.

Fix 1: keep the old static files for a while

The most complete fix is to keep serving the old chunks for a while after the deploy. In the third column of the table, everything just worked, with no errors and no reloads. Chunk names are content hashes, so files from different builds can live side by side without conflicts.

If you deploy by replacing a server or container, merge the previous build's .next/static into the new one before you switch traffic. Copy the old files first and the new ones on top, so the new build wins on any clash:

mkdir -p .next/static-merged
cp -R ../previous/.next/static/. .next/static-merged/
cp -R .next/static/. .next/static-merged/
rm -rf .next/static && mv .next/static-merged .next/static

Checked on node:24-slim: the merged folder had both builds' chunks, and v2's chart chunk was byte-for-byte unchanged. Because the "previous" folder already contains the build before it, this keeps growing with every deploy. Reset it now and then, for example by keeping each build's original static folder and merging only the last few.

If your static files are uploaded to a CDN or object storage (with assetPrefix), the same idea is even simpler: upload the new files without deleting the old ones. Managed platforms solve this for you. Vercel's Skew Protection, for example, routes old clients to the old deployment.

Fix 2: reload once on ChunkLoadError

Even with old files kept around, a tab can outlive them, for instance a laptop that wakes up after a week. For dynamic imports, catch the error and reload, since a reload gets the new HTML and the new chunk names. Guard it so a chunk that's broken for good doesn't cause a reload loop:

const KEY = 'chunk-reload-at';

// After a deploy, an open tab can ask for a chunk the new build no longer has.
// Reload once to get the new build; don't loop if the chunk is broken for good.
export async function importOrReload(load) {
  try {
    return await load();
  } catch (err) {
    const last = Number(sessionStorage.getItem(KEY));
    if (err?.name === 'ChunkLoadError' && !(Date.now() - last < 10_000)) {
      sessionStorage.setItem(KEY, String(Date.now()));
      location.reload();
      return new Promise(() => {}); // the page is going away
    }
    throw err;
  }
}
const { drawChart } = await importOrReload(() => import('./chart'));

In the test, the first click reloaded the old tab onto v2 (page from build v2) and the next click showed chart from build v2. To test the guard, the script also blocked v2's chart chunk, so the error would come back after the reload. The page reloaded exactly once, and the second click showed the ChunkLoadError instead of reloading again.

The error's name was ChunkLoadError with Turbopack, which matches webpack's, so the same check covers both bundlers. Reloading loses component state, so if the lazy code sits behind a form, save drafts somewhere that survives a reload, such as the URL or sessionStorage.

What deploymentId does (and didn't do here)

Next.js has a deploymentId option for version skew:

// next.config.mjs
export default {
  deploymentId: process.env.DEPLOYMENT_ID, // e.g. the git SHA
};

With it set, asset URLs get a ?dpl=<id> query parameter, navigation requests carry an x-deployment-id header, and a mismatch triggers a full page load. Running the same scenarios with deploymentId set to v1 and then v2 gave exactly the same results as the default config. The dynamic import still failed, now as /_next/static/chunks/03o6ntprxxmm-.js?dpl=v1, and the navigation reloads happened either way.

That isn't a bug. On a single server, Next.js already notices the build change on navigation. deploymentId is meant for rolling deployments across several instances and for cache busting at the CDN, where it makes sure each deployment's assets get their own cache entries. As the docs point out, Next.js doesn't route on ?dpl=. Sending an old client to the old files is still the host's or CDN's job, which is Fix 1.

The silent version: a CDN serving old HTML

Statically prerendered pages are served with a header that lets shared caches keep them for a year:

/                                       Cache-Control: s-maxage=31536000
/about                                  Cache-Control: s-maxage=31536000
/_next/static/chunks/03o6ntprxxmm-.js   Cache-Control: public, max-age=31536000, immutable

Year-long caching is right for the hashed chunks and dangerous for HTML. If a CDN honours s-maxage and keeps the v1 HTML after a v2 deploy, new visitors get HTML that references deleted chunks. In the test, the proxy served cached v1 HTML for / and everything else from v2. The page rendered (it's server-rendered HTML), but its page chunk was a 404, so React never hydrated it. The button did nothing, and no error appeared on the page. That's much harder to spot than a ChunkLoadError. If your CDN caches HTML, purge it as part of every deploy.

What about clearing the build cache?

It's unrelated. .next/cache only speeds up the next build, and Next.js recommends keeping it between CI runs. Clearing it changes nothing about which chunk names a build produces or which old ones disappear. Hard-refreshing your own browser makes the error disappear for you, which is why the old advice seemed to work.

Checklist

  • Keep the previous build's /_next/static files available after a deploy, by merging folders or by uploading to a CDN without deleting old files.
  • Wrap dynamic imports in a reload-once handler like importOrReload.
  • Purge cached HTML at the CDN on every deploy, and let only /_next/static be cached long term.
  • Set deploymentId if you run several instances or roll deployments gradually.

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.