When webpack 5's Filesystem Cache Is Worth It: An Architecture Note
An architecture note on webpack 5's persistent filesystem cache: when rebuild frequency justifies it, the smallest safe configuration, why the cache directory is untrusted disposable state, and how to verify invalidation actually works.
19 Mar 2026, 23:54 UTC

If your webpack rebuilds take minutes and your team rebuilds dozens of times a day, the persistent filesystem cache is the single cheapest latency win available: one config block, no new infrastructure. But it introduces a piece of mutable, machine-local state into your build, and that state has trust boundaries and failure modes worth deciding on deliberately rather than discovering after a stale build ships.
This note covers when the cache earns its place, the smallest sound configuration, what you must not trust about the cache directory, how to verify it works, and the conditions that should push you toward a different design. Everything here assumes webpack 5; exact option names and defaults have shifted within the 5.x line, so confirm against the schema of your installed version before relying on specifics.
Requirements: what problem the cache actually solves
webpack 5 can serialize the results of module compilation — parsed ASTs, transformed code, resolver results — to disk and reuse them on the next build. Unchanged modules skip recompilation entirely. Enable it with cache: { type: 'filesystem' }; the default location is node_modules/.cache/webpack.
The cache pays off when three conditions hold:
- Rebuilds are frequent. Developer watch-mode restarts, repeated CI runs on the same runner, or local production-style builds during debugging.
- Rebuild latency is dominated by compilation, not by I/O or asset emission. If your build is slow because of image compression, caching module compilation won't help much.
- The machine persists between builds. A cache is only useful if something survives to be reused.
The contrapositive matters: a one-shot production build in a fresh container has no prior cache to read. Enabling the filesystem cache there adds serialization overhead and disk writes for zero benefit. Keep it off, or scope it to the environments where reuse exists.
The smallest sound configuration
Resist copying a large cache configuration from a blog post. The minimal defensible setup has three parts:
// webpack.config.js
module.exports = {
cache: {
type: 'filesystem',
// Invalidate when the build configuration itself changes.
buildDependencies: {
config: [__filename],
// Add babel.config.js, .browserslistrc, tsconfig.json, etc.
},
// Isolate this cache from other projects or configs on the same machine.
name: 'myapp-production',
},
};Each element exists for a reason:
buildDependenciesis the correctness mechanism. webpack snapshots file contents and timestamps to decide whether cached results are still valid, but it can only snapshot what it knows about. Files webpack loads as part of its own configuration — the config file itself, Babel or PostCSS configs — must be declared, or editing them can silently reuse stale compiled output. This is the most common real-world cache bug.nameprevents cross-contamination. Two projects, or two webpack configurations in one repo, sharing a cache directory and name can collide. In a monorepo, give each package a distinct cache name.- Everything else left at defaults. Tuning
maxAge, compression, or memory allocation before you have measured anything is premature.
If your workflow is purely watch mode in a long-lived process, consider type: 'memory' instead — it never touches disk and avoids the persistence questions entirely, at the cost of losing the cache when the process exits.
Trust and data boundaries
The cache directory is mutable, machine-local, non-authoritative state. Three rules follow:
- Never commit it. Add
node_modules/.cache(or your custom cache path) to.gitignore. A committed cache pollutes diffs, leaks absolute paths between machines, and can be silently reused in environments it was never built for. - Treat it as an integrity risk on shared machines. Anything that can write to the cache directory can influence what your build produces. On shared CI runners, the build system — not an arbitrary prior job — should control the cache path and its lifetime. If you cannot guarantee that, wipe the cache at job start and accept the cold-build cost.
- Never let the build depend on it. A correct build must succeed from a deleted cache. If deleting
node_modules/.cache/webpackbreaks your build, something else is wrong and the cache was masking it.
A subtler boundary issue: loaders or plugins with hidden external inputs — reading environment variables, making network calls, or reading files outside webpack's dependency graph — defeat snapshot-based invalidation. webpack cannot snapshot what it cannot see. If a loader behaves this way, cached builds can go stale with no signal. Audit custom loaders for this before trusting warm builds in CI.
Operational checks
Do not enable the cache and assume it works. Four checks, all runnable locally:
- Cold vs warm timing. Delete the cache directory, run a build, note the time. Run the same build again. The warm build should be measurably faster — on large projects, often dramatically so. If it isn't, the cache isn't hitting, or compilation isn't your bottleneck.
- Invalidation on config change. Touch a file listed in
buildDependencies(e.g., edit a comment inwebpack.config.js) and rebuild. The build should recompile affected modules rather than serving everything from cache. If it doesn't, your invalidation declarations are incomplete. - Clean-slate correctness. Delete the cache directory and confirm a full build succeeds and produces identical output to the warm build (compare emitted file hashes). This proves no hidden dependence on cache state.
- Disk growth. Check the cache directory size across a week of builds. It should plateau, not grow without bound. Unbounded growth on a CI runner eventually becomes a disk-full incident.
Run these from your project root with whatever script wraps webpack (e.g., npm run build). No special permissions are needed beyond normal write access to the project directory; the risk to avoid is running builds as a privileged user whose cache then can't be read by the unprivileged CI user, or vice versa.
Failure modes to design around
- Stale output after config changes not declared in
buildDependencies. Symptom: you changed a Babel preset and the bundle didn't change. Fix: declare the file; when in doubt, delete the cache. - Corrupted cache after interrupted builds. A killed process mid-write can leave the cache in a state webpack must recover from or discard. webpack handles many of these cases internally, but the operational answer is the same: the cache is disposable, so deleting it is always a safe first diagnostic.
- Silent staleness from loader upgrades. Upgrading webpack, loaders, or plugins should invalidate the cache, but verify this after upgrades rather than assuming — run the clean-slate check above after any toolchain bump.
- Cross-project contamination from shared cache names, covered above.
In every case, the recovery procedure is identical and boring by design: delete the cache directory and rebuild. That is the payoff of treating the cache as non-authoritative.
Conditions that change the design
Revisit this setup when any of the following become true:
- Fully ephemeral CI runners. If every job starts on a fresh machine, a local filesystem cache is dead weight. Either skip persistence in CI or evaluate a shared/remote cache strategy — which introduces its own trust questions (who can write it?) beyond this note's scope.
- Monorepo growth. More packages means more cache names to manage and more disk consumed. At some scale, a centrally managed cache location with explicit retention beats per-package defaults.
- Loaders with non-snapshotable inputs. If you adopt a loader that reads external state, either wrap it so the input becomes a declared dependency, or disable caching for the builds that use it.
The design principle throughout: the cache is an optimization layered on a build that must be correct without it. Keep the configuration minimal, the directory disposable, and the verification habitual, and it will stay an asset instead of becoming the build's most confusing component.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.