Skip to content

Vitest 5.1 Beta: --last-failed and --since-last-run, Measured

Vitest 5.1.0-beta.1 adds --last-failed, --since-last-run and Node timer mocks. I ran all three on a 60-file suite: 1,744 ms down to 341 ms per rerun.

· · 8 min read
Bar chart of Vitest 5.1 beta rerun times: 1744 ms full run against about 340 ms for last-failed and since-last-run

Quick Take

Vitest 5.1.0-beta.1 shipped on 19 October 2026. I ran its two new selective flags on a generated 60-file suite: a full run took a median 1,744 ms, a --last-failed rerun 344 ms and a --since-last-run rerun after one edit 341 ms. Fake timers for node:timers work only behind a new opt-in flag. A full run is no faster than on 5.0.5.

Everybody wants a faster test run. Few people ask the better question: why is the whole suite running again for one edited file?

Vitest 5.1.0-beta.1 went out on 19 October 2026, four days after 5.0.5. It sits on the beta dist-tag, so npm install vitest still gives you 5.0.5. The release notes list two flags that go straight at that question, plus a fix for fake timers that I'd been waiting on. I installed the beta in a scratch project and tried each one.

I'll say up front what I didn't test. Node 22.23.3 has no Temporal global, so the new Temporal.Instant support in vi.setSystemTime() stayed out of reach. The per-retry attempt results and the browser-mode additions (import-map mocking, the pointer API) also stayed untouched. Everything below is a pre-release on one machine.

What does the 5.1 beta actually add?

Reading the release notes, the user-facing features are these:

  • --last-failed runs only the tests that failed in the last run.
  • --since-last-run runs only tests whose recorded dependencies changed since their last run.
  • Fake timers now cover node:timers and node:timers/promises.
  • Temporal.Instant and Temporal.ZonedDateTime work with vi.setSystemTime().
  • Retry and repeat attempts expose a per-attempt result.
  • Browser mode can mock modules through an import map, and the trace view loses its experimental label.

There's also one performance entry, reusing a module cache during static collection, and a handful of fixes. The browser default port changes to 23315, which matters only if you hardcoded the old one.

How I measured it

The setup is deliberately boring. A generator script writes 60 test files, each importing its own tiny module and sleeping 40 ms before one assertion. I broke two of them on purpose, so a full run reports 2 failed | 58 passed (60). The machine is an Apple M1 on Node 22.23.3, installed with pnpm 12.8.1.

Wall time comes from a stopwatch around the whole vitest run process, five runs per case, median reported. That includes process start, which the Duration line Vitest prints leaves out. Each version lives in its own directory so they don't share a cache, and I ran one version's five rounds right after the other's.

The runnable version lives in the vitest-5-1-beta-selective-reruns example, and it asserts the file counts below instead of just printing them.

How fast is --last-failed?

Here's the whole sequence from my terminal, trimmed:

npx vitest run                  # 2 failed | 58 passed (60)
npx vitest run --last-failed    # Last Failed  2 of 60 test files

The rerun executed exactly the two broken files. Wall time was a median 344 ms against 1,744 ms for the full run, five rounds each (342 to 348 ms against 1,717 to 1,830 ms). That's about five times faster on a suite where every file waits 40 ms, so a real project with slower files gains more in absolute seconds and about the same in proportion.

I fixed one file and ran it again. The list shrank: Last Failed 2 of 60 while one passed and one still failed, then 1 of 60 for the survivor. After I fixed the last one, one more run passed, and the next call printed this:

No failed tests found, exiting with code 1

Look at that exit code. Nothing failed, and the command still exits 1. A pre-push hook that runs vitest run --last-failed will block every push once the suite is green, and so will a CI step that treats non-zero as a failure. Guard it with || true or check for the message. I'd call it a rough edge rather than a bug, since "you asked for failures and there are none" is a defensible exit state, but it will surprise someone.

Where does Vitest remember the failures? I found the file in node_modules/.vite/vitest. Move that folder away and --last-failed forgets everything, printing the same "No failed tests found" line. Delete node_modules in CI, and the flag has nothing to work from. I haven't wired this into a CI pipeline, so I can't tell you whether caching that folder between jobs is a good idea.

Vitest 5.1's --last-failed turned a 1,744 ms full rerun into 344 ms, but it exits with code 1 when nothing failed, so guard any hook that calls it.

Share this Post on X Bluesky

What does --since-last-run do differently?

--changed has been around for a while and asks git what differs. The new flag works another way. The help text says it runs "only the tests whose recorded dependencies were modified since the last run of the test, and the tests that failed in their last run", and that it enables --changed, --experimental.recordDependencies and an mtime provider. In plain terms, Vitest writes down which files each test imported, then compares modification times next time.

I ran these scenarios against the same suite:

ScenarioFiles runWall time
First call, no record yet601.6 s (Vitest's own figure)
Second call, nothing touched0202 ms median
One source file edited1341 ms median
A shared helper imported by 15 tests edited15479 ms (Vitest's own figure)
A test left failing, nothing touched1136 ms (Vitest's own figure)

Two details stood out. The first call is cold, so it runs everything, and the "nothing touched" case prints No changed files found, exiting with code 0. That's a clean exit, unlike its sibling. And the failing test came back by itself on the next call, exactly as the help text promises.

The dependency tracking behaved the way I'd hope. Editing src/m12.ts ran one file, and editing a helper that fifteen files import ran fifteen. I didn't create a git repository in the scratch folder at all, which is the practical difference from --changed. The 202 ms floor for an empty selection is mostly process start.

One caveat. mtime comparison is cheap, and it also means a touch counts as a change. Run a formatter over a hundred files and you'll have a hundred files to rerun.

Do fake timers now cover node:timers?

Yes, with an opt-in that the release notes don't mention. The pull request description explains it: Vitest patches the default export of node:timers, then uses Node's syncBuiltinESMExports so named imports see the fake too, and guards all of it behind useFakeTimers({ fakeNodeBuiltins: true }).

My test is short:

import { test, expect, vi } from 'vitest';
import { setTimeout as sleep } from 'node:timers/promises';

test('sleep(60s) under fake timers', async () => {
  vi.useFakeTimers({ fakeNodeBuiltins: true });
  let done = false;
  const p = sleep(60_000).then(() => { done = true; });
  await vi.advanceTimersByTimeAsync(60_000);
  await p;
  expect(done).toBe(true);
  vi.useRealTimers();
});

On the beta with the option, it passed in about 100 ms. Without the option, the same test hung until the 5,000 ms timeout. On 5.0.5 it timed out whether or not I passed the option. A plain setTimeout imported from node:timers behaved the same way: zero calls after advancing by 5,000 ms until I set the flag.

If you've ever stubbed setTimeout from node:timers/promises by hand because fake timers ignored it, this is the feature. It's opt-in, though, so existing suites change nothing until someone sets the flag.

Is 5.1 faster on a full run?

No, and I'd rather say so than let the speed claims above blur. Five full runs each, wall time in milliseconds:

5.0.5          1817 1740 1746 1713 1741   median 1741
5.1.0-beta.1   1748 1767 1793 1841 1734   median 1767

That's a 26 ms gap in the wrong direction, well inside the spread of either column. The release notes list one performance change, and my synthetic suite has no heavy imports for it to help with. A real project could show a gain. Mine can't.

Both versions printed the same hint after a run, that isolate: false would be at least 791 to 998 ms faster by reusing workers. That's not a 5.1 feature, since 5.0.5 prints it too. On a suite this small and this independent it's probably true, and it costs you test isolation, so read up before flipping it.

Should you install the beta?

My opinion, and you may disagree: don't put it in a shared lockfile yet. A beta can change a flag's behavior before the final release, and the "exit 1 when nothing failed" behavior is the kind of detail that gets revisited. Try it in a branch, run --since-last-run on your own repository and see whether the dependency tracking matches how your tests actually import code.

What would I do on a laptop today? Pin the beta in one throwaway folder, alias --last-failed for the red-green loop and leave the project on 5.0.5. If you're still on Vitest 4 or earlier, the Vitest 5 migration guide comes first, and the Vitest 5 feature rundown covers what 5.0 already gave you. Coming from Jest instead? Start with the Jest to Vitest 5 migration.

How much of your day goes to rerunning tests that can't have changed? Measure that number before you decide the flags matter.

Frequently Asked Questions

What does vitest --last-failed do in 5.1?
It runs only the test files that failed in the previous run. In my 60-file test, two files failed, and the rerun executed exactly those two in a median 344 ms of wall time against 1,744 ms for the full run. When nothing failed last time it prints 'No failed tests found' and exits with code 1, which can break a script that treats any non-zero exit as a failure.
What is the difference between --since-last-run and --changed?
--changed asks git which files differ. --since-last-run records which files each test imported and compares modification times with the previous run, so it also works without a git repository. The help text says it enables --changed, a dependency-recording option and an mtime provider, and it reruns tests that failed last time as well. The first run is cold and executes every file.
How do I fake node:timers/promises in Vitest 5.1?
Pass an option: vi.useFakeTimers({ fakeNodeBuiltins: true }). Without it, a sleep from node:timers/promises hung until the 5-second test timeout on both 5.0.5 and 5.1.0-beta.1 in my run. With the option, a 60-second sleep resolved under advanceTimersByTimeAsync on the beta, while 5.0.5 still failed even with the option set.
Is Vitest 5.1 beta faster than 5.0.5?
Not on a full run of my generated suite. Five runs each gave medians of 1,741 ms for 5.0.5 and 1,767 ms for 5.1.0-beta.1, a difference inside the noise. The gain is in rerunning less, not in running faster. Real projects with heavier imports may show something different, and I didn't test that.