Using Mocha’s Built‑In Retries to Stabilize Flaky Integration Tests
Learn how Mocha’s native retry feature works, see a concrete example of retrying a flaky API call, and understand the trade‑offs and best‑practice guidelines for using it in CI pipelines.
16 Jun 2026, 00:50 UTC

The problem: flaky integration tests slowing down CI
When a test suite touches networks, databases, or third‑party services, occasional timing glitches can cause a test to fail on the first run but pass on a second attempt. If the failure is treated as a hard error, the whole pipeline stops, forcing developers to rerun the job manually. This wastes time and obscures whether the failure is truly a defect or merely environmental noise.
How Mocha’s retry mechanism works
Mocha provides a native retry feature that can be applied at the test level, the suite level, or globally via the command line. Inside a test or describe block you call this.retries(n) where n is the number of additional attempts after the initial failure. The function must be a regular function (function() { … }) because it relies on Mocha’s test context (this); arrow functions silently ignore the call and leave retries disabled.
Each retry re‑executes the entire test, including its beforeEach and afterEach hooks, but does not re‑run the root‑level before or after hooks. Suite‑level this.retries(n) inherits to nested suites, making it easy to scope retries to a known‑flaky area such as an integration folder.
Importantly, each attempt receives the full timeout budget. If a test has a 2‑second timeout and you allow two retries, the worst‑case wall‑clock time for that test is three times the timeout (6 seconds). This can inflate suite runtime when retries are used liberally.
Worked example: retrying a flaky API call
Consider a test that posts to a service that occasionally returns a 502 error due to load‑balancer latency. The test uses a module‑level counter to simulate the flakiness: the first call throws, subsequent calls succeed.
// test/apiRetry.test.js
let callCount = 0;
function flakyPost() {
callCount += 1;
if (callCount === 1) {
const err = new Error('502 Bad Gateway');
err.code = 502;
throw err;
}
return Promise.resolve({ status: 200 });
}
describe('Flaky API endpoint', function () {
// Allow one retry for every test in this suite
this.retries(1);
it('should eventually succeed', async function () {
const resp = await flakyPost();
// Using a generic assertion style
if (resp.status !== 200) throw new Error('Expected 200');
});
});
When Mocha runs this file:
- The test executes,
flakyPostthrows, and Mocha catches the error. - Because
this.retries(1)is set, Mocha schedules a second attempt. - On the second run
callCountis 1, the function resolves, and the assertion passes. - Mocha reports the test as passing and notes that one retry was used (visible in the spec or dot reporter output).
To achieve the same behavior from the command line you could run:
mocha --retries 1 test/apiRetry.test.js
This flag applies the retry count to every test in the invocation unless overridden inside the code.
Trade‑off: stability versus hidden defects
Retries are valuable for nondeterministic external factors—network blips, temporary service throttling, or clock‑skew in CI containers. They give the pipeline a chance to succeed without manual reruns.
The downside is that they can mask genuine bugs in your own code. A race condition that only shows up under load might appear as a flaky test; enabling retries could let the test pass intermittently, delaying the fix. Because each attempt consumes the full timeout, a slow‑but‑correct test can also cause unnecessary latency.
A pragmatic policy that many teams adopt:
- Disable retries for unit tests (they should be deterministic).
- Enable retries only in clearly marked integration or end‑to‑end suites.
- Track the number of retries per run (most reporters emit retry counts) and treat a rising trend as a signal to investigate the underlying flakiness.
- Keep the retry count low (usually 1) to limit runtime impact.
Actionable closing
If you see integration tests failing sporadically, start by adding a suite‑level this.retries(1) to the describe block that contains those tests. Verify the behavior locally: the test should fail on the first run and pass on the second, with the reporter indicating a retry was used. Monitor your CI logs for retry counts; if they stay low and stable, you have gained resilience without hiding real defects. If the count climbs, treat it as a prompt to examine the external dependency or the test’s timing assumptions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.