Skip to content
Rémy Duthu Rémy Duthu
April 25, 2026 · 5 min read

Why jest.retryTimes() is hiding bugs in your test suite

Why jest.retryTimes() is hiding bugs in your test suite

Adding retryTimes(3) feels like fixing flakes. It is not. It is throwing away the data you need to find the bug, then telling CI everything is fine.

Someone on your team has had a long week. The CI is failing on a flaky test, the merge queue has been stuck for an hour, the on-call engineer is in a meeting, and the PR has to ship before the demo. They open jest.config.js, add this:

jest.retryTimes(3);

CI goes green. The PR ships. Everyone gets back to work.

This is the moment your test suite started lying to you.

What retryTimes actually does

The Jest docs are honest about it: retryTimes(n) re-runs a failing test up to n times and reports the result of the last attempt. If the test passes on attempt 2 of 4, the suite is green. The first failure is discarded.

The implication: every test in your suite is now allowed to fail, as long as it fails fewer than n + 1 times in a row. A bug that has a 25% failure rate (one in four attempts) statistically passes 99.6% of the time with retryTimes(3). You will see it in production. You will not see it in CI.

This is not a hypothetical. We watch it happen in customer suites. The ones that adopted retryTimes as a permanent policy two years ago have CI green rates above 99% and a long tail of “weird user reports” that nobody can reproduce. The ones that did not adopt it have noisier CI and shorter incident lists.

The four categories of flake retryTimes confuses

Calling something “flaky” treats four very different problems as one:

  1. A real bug with a low failure rate. A race condition, an off-by-one in a date library, a connection pool that occasionally returns a closed connection. The failure is real every time. The retry happens to land in the 75% of attempts where the race resolves favorably.
  2. Test pollution. A previous test left timers, mocks, or module state behind. The first run of your test sees the polluted world; the retry runs after teardown finished and sees a clean one. The test you wrote is fine. The test that ran before it is the bug.
  3. Environment instability. A network call to an external service times out. A CI runner with less headroom than your laptop loses a tight setTimeout race. Your test is correct, the environment is the problem.
  4. A genuinely non-deterministic test. You wrote Math.random() and forgot. Or you used Date.now() for keys. Or you depend on Map iteration order with non-deterministic input. The test is broken even though the code is fine.

Only one of these (3) is the case where retrying is harmless. Two of the four (1 and 2) are bugs you want to find. One (4) is a broken test that needs rewriting. retryTimes treats them all the same: hide the failure, ship the PR.

What “flaky” should mean instead

A flaky test is a test you cannot trust as a binary signal until you have more data. Not “a test that occasionally fails”. The difference matters because it changes the response.

When the response is “treat each failure as a real bug, and only quarantine after evidence accumulates”, you find bugs early. When the response is “set a retry budget”, you ship them.

The infrastructure version of this is the difference between:

  • Quarantine: the test still runs, the result is recorded and surfaced, but a failure does not block the merge. Engineers see the noise and choose when to invest in fixing it.
  • Retry: the test still runs, the failure is discarded, and the merge proceeds as if everything is fine.

Quarantine keeps the signal. Retry destroys it.

The case people make for retryTimes

The honest argument for retryTimes is that engineering time is finite, and a known-flaky test that mostly passes is not worth blocking the queue over. That is a real trade-off.

The dishonest version is “we will fix it later”. retryTimes adds zero pressure to fix anything. The test is now “passing”, which means the ticket gets closed and the bug ages indefinitely. We have customers who turned on retryTimes in 2024 and have not removed a single retry rule since.

If you want the trade-off without losing the signal, use a flaky-test detection layer that:

  1. Runs the test as written, no retries at the Jest level.
  2. Records pass/fail per attempt across many runs.
  3. Quarantines (does not retry) once a confidence threshold is breached.
  4. Surfaces the quarantine in the dashboard, with a link to the PR or the commit that introduced the noise.
  5. Lifts quarantine automatically when the test stabilizes.

Mergify Test Insights does this. So do a couple of other tools in the space (we have a comparison page for one of them). The shape is what matters: detection plus quarantine plus visibility, not retry.

When retryTimes is acceptable

There are exactly two cases where adding retryTimes to your config is the right call:

  1. Active triage of a specific known-broken test. You know the test is broken. You have a fix in flight. You add retryTimes with an inline comment naming the ticket and a removal date. When the date passes and the retry is still there, your linter complains.
  2. An external-service flake you cannot fix. A third-party API returns 502 once a day. You retry the network call (or better, you mock it) rather than letting the test fail. This is not really a retryTimes use case; you should be retrying inside the test, not retrying the test.

Anything beyond those two is hiding a bug. Probably more than one.

What to do this afternoon

If you have retryTimes somewhere in your config and you are not sure why, here is a five-minute exercise:

  1. Remove it.
  2. Run the suite three times (or however many runs retryTimes was masking).
  3. Note every test that fails in any of the three runs.
  4. For each one, decide: is this a real bug, test pollution, environment, or a broken test?

Most of what comes back as “flaky” is one of the first two. Those are the ones you want to fix. The third is rare in well-written suites; the fourth is common but obvious once you look at the test.

The retry was hiding the answer to a question you should have been asking.

More patterns where Jest lies to you

The retry-hides-bugs pattern is one of eight Jest flake patterns we see most often on Mergify Test Insights. The others are mostly about state crossing test boundaries: fake timers leaking into the next test, snapshots drifting on shared state, module mocks hoisting above imports, test.concurrent racing on counters.

Same theme: something wasn’t reset. The fix is usually small. The hard part is being willing to look at the failure instead of retrying past it.

Test Insights

Tired of flaky tests blocking your pipeline?

Test Insights detects flaky tests, quarantines them automatically, and tracks test health across your suite.

Try Test Insights

Recommended posts

Claude Didn’t Kill Craftsmanship
February 4, 2026 · 5 min read

Claude Didn’t Kill Craftsmanship

AI doesn't remove craftsmanship: it moves it. The goal was never to protect the purity of the saw. It's to build good furniture. Engineers can now focus on intent, judgment, and product quality instead of translating tickets into code.

Rémy Duthu Rémy Duthu
Why WARNING Has No Place in Modern Logging
October 8, 2025 · 4 min read

Why WARNING Has No Place in Modern Logging

Most systems drown in meaningless WARNING logs. They waste money, obscure real errors, and help no one. Here’s why your next logging cleanup should start by deleting WARNING — and how structured logs make your production systems clearer, cheaper, and safer.

Mehdi Abaakouk Mehdi Abaakouk
AI Won't Replace Code Reviews, But It Can Fix Them
October 1, 2025 · 5 min read

AI Won't Replace Code Reviews, But It Can Fix Them

Code reviews often fail not because the code is wrong, but because no one knows why it was written that way. This post explores how AI-generated comments can add missing intent to pull requests—making both human and AI reviews smarter, faster, and more effective.

Alexandre Gaubert Alexandre Gaubert