#21188·eslint

Cache stores end-of-run file hash with lint-time results, so a file edited during a run poisons the cache

Author: marcospgpCreated Aug 4, 2026Updated Sep 16, 2026
LabelsStale

Environment

Node version: v24 (via Bun 1.3.x runner, also reproduces under plain Node) npm version: n/a (bun) ESLint version: 10.7.0 (file-entry-cache 8.0.0, flat-cache 4.0.1) Operating System: macOS 15

What parser are you using?

Default (Espree)

What did you do?

We run eslint . --cache --cache-strategy content in a monorepo where editors and other tooling keep writing to files while a lint run is in progress. A full run takes over a minute here, so mid-run writes are routine.

What did you expect to happen?

A cache entry should only be served for a file whose content matches the content that was actually linted.

What actually happened?

The cache can permanently store a fresh content hash paired with lint results from older content. After that, later runs serve wrong results (stale errors for code that no longer exists, or a stale clean verdict for code that now has problems) and keep serving them until the cache is deleted, because from the cache's point of view the entry is valid.

The mechanism is in file-entry-cache's reconcile(), which ESLint calls at the end of the run. reconcile() re-reads every file from disk at that point and computes its checksum then, but the meta.results it stores alongside were attached when the file was linted, possibly minutes earlier:

javascript
// file-entry-cache 8.0.0, cache.js
var meta = useChecksum
  ? me._getMetaForFileUsingCheckSum(cacheEntry) // re-reads the file NOW
  : me._getMetaForFileUsingMtimeAndSize(cacheEntry);
cache.setKey(entryName, meta); // meta.results still comes from lint time

So any write to a source file between its lint and the end of the run produces an entry whose hash says "current content" while its results describe the previous content. lint-result-cache.js then accepts it on the next run:

javascript
const changed = fileDescriptor.changed || fileDescriptor.meta.hashOfConfig !== hashOfConfig;
if (changed) return null;
return fileDescriptor.meta.results;

--cache-strategy content does not protect against this, since the checksum stored is the checksum of the new content. The mtime strategy has the same shape of hole because the mtime is also read at reconcile time. Note the trigger is any concurrent writer (an editor save, a formatter, a git checkout), not a second ESLint process, so a lock around ESLint itself would not close it.

Timeline for a minimal reproduction:

  1. Start eslint . --cache --cache-strategy content on a repo big enough that the run takes a while.
  2. After a file has been linted but before the run finishes, edit that file so its lint verdict would change (introduce or remove an error).
  3. Let the run finish. The cache now holds the new content's hash with the old content's results.
  4. Run ESLint again: it reports the old results for the current content, and keeps doing so on every subsequent run.

We hit this in practice as a lint failure describing source that was no longer on disk, which survived any number of re-runs until .eslintcache was deleted.

The fix direction would be hashing the buffer that was actually linted at lint time instead of re-reading at reconcile time. I saw in #20801 that the v11 upgrade of file-entry-cache is pending; per the review comments there, v11's reconcile() updates metadata for all files rather than fewer, so it looks like the rewrite would widen this rather than close it, which seemed worth flagging before that lands.

Link to Minimal Reproducible Example

The race needs a mid-run write, so a live repro link can't capture it; the timeline above reproduces it on any repo with a multi-second lint run.

Participation

  • I am willing to submit a pull request for this issue.

AI acknowledgment

  • I did not use AI to generate this issue report.
  • (If the above is not checked) I have reviewed the AI-generated content before submitting.

Additional comments

None.