</>CodeWithKarani

Vite '504 Outdated Optimize Dep' Explained: The Dependency Cache Race You Cannot Fully Avoid

Karani GeoffreyKarani Geoffrey9 min read

You are working happily in the Vite dev server. You click a link to a route you have not visited yet, or you install a new package and import it, and the page goes white. The console is full of red: a request for a file under node_modules/.vite/deps/ came back 504, hot module reload is dead, and the only thing that brings it back is a hard reload or killing the dev server and restarting it. Ten minutes later it happens again.

The status code makes it look like a server or a network problem. It is neither. Nothing is down, your network is fine, and the file it is asking for really did exist a moment ago. What you are watching is a cache-invalidation race inside Vite's dependency pre-bundling, and the giveaway is right there in the error text: not "not found", but outdated.

Some versions of this are fixable and some are an accepted rough edge that the Vite team has explicitly declined to fully solve, because the underlying race cannot be closed without giving up the thing that makes the dev server fast. So this article does two things: it gives you the immediate unstick, and it tells you honestly which variant you can prevent and which you can only mitigate.

Vite pre-bundles your dependencies and serves them at a version-hashed URL. When the optimizer re-bundles, the hash changes, and a request for the old hash returns 504 (Outdated Optimize Dep).

  • Right now: hard-reload the page. If that does not clear it, restart the dev server with --force, or delete node_modules/.vite and restart.
  • Reduce it: list the dependencies you know you use in optimizeDeps.include so Vite bundles them up front instead of discovering them mid-session and re-optimising.
  • The lazy-route-navigation variant is still open and was in one case closed "Not planned". There is no guaranteed fix for that one, only mitigation.

The exact error

GET http://localhost:5173/node_modules/.vite/deps/xxx.js?v=abc123
net::ERR_ABORTED 504 (Outdated Optimize Dep)

Two parts of that line are the entire diagnosis. The ?v=abc123 query is a version hash Vite stamps onto every pre-bundled dependency URL. And the message is Outdated Optimize Dep, not "not found" or "server error". Vite is telling you, in plain words, that the browser requested a version of a pre-bundled dependency that is no longer the current one. The 504 is Vite's own dev server deliberately refusing to serve a stale artefact, not an upstream timeout.

Why the version hash exists, and why it goes stale

When you start vite, before it serves anything, it scans your source for imports of third-party packages and pre-bundles them with esbuild into node_modules/.vite/deps/. There are two reasons it does this. First, many npm packages ship as dozens or hundreds of tiny internal modules, and a browser fetching each one over native ESM would fire a storm of requests; bundling each dependency into one file keeps the request count sane. Second, some packages still ship CommonJS, which browsers cannot import natively, so Vite converts them to ESM once, up front.

Each pre-bundled file is served with a ?v= hash derived from the current state of your dependencies and config. Your application code imports these deps, and Vite rewrites those imports to point at the hashed URL. As long as the hash the browser holds matches the hash Vite is serving, everything is fast and cached hard.

The problem is that the dependency graph is not frozen while the server runs. Vite discovers dependencies lazily: it only knew to pre-bundle what it saw during the initial scan, plus whatever it has encountered since. When you navigate to a lazily-loaded route that imports a package Vite had not seen, or you add a new dependency, Vite runs a re-optimization. Re-optimizing changes the hash. Now every dependency URL the browser is holding, stamped with the old hash, is stale. A request for one of them arrives, Vite sees a hash it no longer serves, and returns 504 (Outdated Optimize Dep). Vite normally responds to a re-optimize by asking the browser to do a full reload so it picks up the new hashes, but there is a window where an in-flight request for an old-hash file loses the race, and that is the 504 you see.

The re-optimize race, on a timeline 1. Startup: Vite pre-bundles deps at hash v1, browser loads v1 URLs everything fast, everything cached 2. You navigate to a lazy route that imports a never-seen package Vite discovers a new dependency it did not pre-bundle 3. Vite re-optimizes: new hash v2, old v1 URLs are now stale a full reload is requested, but requests are already in flight 4. An in-flight request for a v1 file loses the race, Vite refuses it 504 (Outdated Optimize Dep)
Nothing failed. Vite deliberately declined to serve an artefact it had just superseded.

The two triggers, and why they are not equally fixable

There are two distinct ways to land here, and knowing which you have decides whether you can eliminate it or only reduce it.

TriggerWhat happensCan you fully fix it?
Adding or updating a dependency while the server runsThe dep graph genuinely changed, so a re-optimize is correct; you just got caught mid-flightYes, in practice: a restart plus optimizeDeps.include makes it rare
Lazy-route navigation racing a re-optimizeNavigating to a code-split route surfaces a dep mid-session and the re-optimize races the route's own requestsNo guaranteed fix; it is an accepted rough edge, mitigate only

The first is a normal consequence of changing your dependencies while the dev server is live, and it settles once Vite knows about the new package. The second is the one that generates long, unresolved GitHub threads, and at least one report of it was closed Not planned, meaning the maintainers consider it an inherent trade-off of lazy discovery rather than a bug with a pending fix. Do not spend a day hunting for the setting that makes the lazy-route variant disappear; there isn't one.

Step 1: Unstick it right now

In order of escalating disruption:

# 1. Hard reload the browser tab first (Ctrl+Shift+R / Cmd+Shift+R).
#    The page re-requests every dep at the current hash.

# 2. If it persists, restart the dev server forcing a fresh pre-bundle:
npm run dev -- --force

# 3. If it still persists, nuke the pre-bundle cache and restart:
rm -rf node_modules/.vite
npm run dev

--force tells Vite to ignore the cached optimizer output and re-run pre-bundling from scratch, which resolves the case where the on-disk cache and what the server thinks it is serving have diverged. Deleting node_modules/.vite is the same thing by hand and is the reliable reset when a lockfile change or a crashed previous run left the cache in an odd state. Expect a slightly slower first load after either, because Vite is re-bundling.

Step 2: Pre-declare your dependencies so re-optimization is rare

The re-optimize only fires when Vite meets a dependency it had not already bundled. If you tell it up front which packages you use, it bundles them all during the initial scan and stops discovering them mid-session. Add the ones behind lazy routes to optimizeDeps.include:

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  optimizeDeps: {
    include: [
      // deps that only get imported inside lazy-loaded routes,
      // so Vite's initial scan misses them
      'chart.js',
      'date-fns',
      '@my-scope/heavy-widget',
    ],
  },
})

This is the single most effective mitigation, and it targets the exact mechanism: pre-declared deps are bundled into the first hash and never trigger a mid-session re-optimize. The ones to list are precisely those that are only reached through a dynamic import() or a lazily-loaded route, because those are the ones the startup scan cannot see. If you rarely change dependencies, this alone makes the 504 go from a daily annoyance to a once-in-a-while event.

Step 3: Reduce how often you hit the lazy-route variant

Since you cannot fully eliminate the lazy-navigation race, reduce your exposure to it:

  • Keep optimizeDeps.include current as you add code-split routes. A new lazy route that imports a fresh package is a new candidate for the race until you list its deps.
  • After a dependency change, restart the dev server rather than relying on the live re-optimize. A clean restart bundles everything once; a live add is exactly the window where the race lives.
  • If a specific package is pathological, you can exclude it from pre-bundling with optimizeDeps.exclude, but do this sparingly and only for a package that is already valid ESM, because excluding a CommonJS package will break its import entirely.

Verifying your mitigation is working

You want to confirm two things: that Vite is not re-optimizing during normal navigation, and that your include list actually took effect. Run the dev server with debug output for the optimizer:

DEBUG=vite:deps npm run dev

On startup you should see Vite pre-bundle the full set, including the packages you added to include. Then click through your app, especially the lazy routes that used to trigger the 504. If the mitigation is working, navigating those routes produces no new "re-optimizing dependencies" log lines and no new ?v= hash, because everything was already bundled at startup. A fresh re-optimize log line the moment you hit a route is your signal that the route imports a dep still missing from include; add it and repeat.

What people get wrong

Treating the 504 as a proxy or CORS problem. The status code sends people down a rabbit hole of dev-server proxy config and network debugging. It is not a network issue at all; it is Vite's own server intentionally rejecting a stale artefact. The word Outdated in the message is the tell. No amount of proxy tweaking touches the cause.

Deleting node_modules and reinstalling every time. A full rm -rf node_modules && npm install does clear it, because it also wipes node_modules/.vite, but it is a sledgehammer that costs minutes each time. The cache lives in node_modules/.vite specifically; delete just that, or use --force, and skip the reinstall.

Disabling dependency optimization entirely. Some threads suggest turning off pre-bundling to dodge the 504. This trades a occasional white screen for a permanently slow dev server and outright breakage on any CommonJS-only dependency, since those rely on the conversion step. You are removing the feature to avoid one of its edge cases. Keep pre-bundling on and use include instead.

Expecting optimizeDeps.include to fix the lazy-route race completely. It makes it much rarer by pre-bundling the deps, but the race between a route's own requests and a re-optimize can still occur in principle. If someone tells you a config line makes the lazy variant impossible, they are describing the reduction, not a guarantee. This is an accepted rough edge; manage it, do not expect it to vanish.

When it is still broken

  • It fires on every startup, not just navigation. That points at a config or lockfile that keeps invalidating the cache. Check whether something in your build writes to vite.config or your dependencies on each run, and confirm your lockfile is committed and stable, because a changing dependency tree re-hashes on every start.
  • A monorepo or linked package keeps re-optimizing. Locally linked workspace packages change often and defeat the cache. Consider adding them to optimizeDeps.exclude if they are ESM, or ensure the linked package is built to a stable output that Vite can treat as a normal dependency.
  • You are also fighting memory or module-resolution issues. If the dev server is unstable in other ways too, the pre-bundle cache is only one suspect. My notes on dev-server memory growth and module resolution under dynamic requires cover adjacent failure modes in the same tooling family.
  • Check the Vite version before building elaborate workarounds. The behaviour around re-optimization and the reload it triggers has been tuned across releases. Reproduce on the current stable version, and read the dependency pre-bundling guide for what your version does, before you hardcode a workaround that a newer Vite has already improved.

The honest summary: this is a race between a cache that keys on a hash and a graph that changes while you work, and Vite chose to fail loudly with a 504 rather than serve you a stale module and let you debug a phantom. Pre-declare what you use and the first trigger nearly disappears; for the lazy-route trigger, keep the reset commands handy and accept that "rare" is the realistic target, not "never".

Frequently asked questions

What does 504 Outdated Optimize Dep mean in Vite?
Vite pre-bundles your dependencies and serves each one at a version-hashed URL (the ?v= query). When Vite re-optimizes, the hash changes and the old URLs become stale. A request for an old-hash file returns 504 (Outdated Optimize Dep), which is Vite's own dev server deliberately refusing to serve a superseded artefact. It is not a network timeout or a proxy problem, despite the status code.
How do I fix the Vite 504 Outdated Optimize Dep error immediately?
Hard-reload the browser tab first (Ctrl+Shift+R or Cmd+Shift+R) so it re-requests deps at the current hash. If that does not clear it, restart the dev server with npm run dev -- --force to force a fresh pre-bundle. If it still persists, delete node_modules/.vite and restart. There is no need to reinstall node_modules; only the .vite cache directory matters.
How do I stop Vite re-optimizing dependencies mid-session?
List the packages you use, especially those imported only inside lazy-loaded routes, in optimizeDeps.include in vite.config. Vite's startup scan cannot see deps behind dynamic imports, so it discovers them mid-session and re-optimizes, which is what causes the 504. Pre-declaring them bundles everything into the first hash so no mid-session re-optimize is triggered. This is the single most effective mitigation.
Can the Vite 504 error be completely eliminated?
The variant from adding or updating a dependency while the server runs can be made rare with a restart and optimizeDeps.include. The lazy-route navigation variant cannot be fully eliminated; it is a race between a route's requests and a re-optimize, and at least one report was closed 'Not planned' as an accepted rough edge. Manage it with pre-declaration and the reset commands rather than expecting it to vanish.
#Vite#HMR#esbuild#Frontend Tooling#optimizeDeps
Keep reading

Related articles