Cypress: Debugging, Studio, Plugins & Reporters
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
A Cypress test that fails in a pipeline is only useful if you can find out why, and a suite that you cannot automate, report on
or keep fast stops being used. This page covers the tools around the test code itself: the debugging commands and the Command
Log, Cypress Studio, the Node side of a project (setupNodeEvents, the Node events, cy.task and plugins), reporters, code
coverage, test retries and test performance. It is written for Cypress 16.2.0 and was checked on 25 October 2026. Every
configuration file and spec below was run against the
Bookshelf application with cypress run in headless Chrome 155
and in Electron, and the output excerpts are real. The parts that need a window (cypress open, the Command Log, Studio) or an
account (Cypress Cloud) were documented but not run, because the machine that produced these examples had no display to drive;
each such place says so.
The ideas behind the command queue and retry-ability are in Core Concepts,
cy.intercept and cy.request in
Network, Stubs, Spies & Clocks, cy.session and cy.env()
in Sessions, Authentication & Cross-Origin, and
recorded runs, Test Replay and parallelisation in Cypress Cloud. The tool-neutral
debugging ideas (what a screenshot, a video and a trace give you) are in
Screenshots, Videos & Traces.
The example project
All examples share one project. The configuration below registers every Node event discussed later, a junit reporter that
writes one file per spec, retries for cypress run, and a few tasks. The base URL comes from the environment so that each run
can target its own Bookshelf server.
import { defineConfig } from 'cypress';
import { appendFileSync, mkdirSync } from 'node:fs';
export default defineConfig({
retries: { runMode: 1, openMode: 0 },
reporter: 'junit',
reporterOptions: {
mochaFile: 'results/junit-[hash].xml',
toConsole: false,
},
e2e: {
baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
video: false,
screenshotsFolder: 'cypress/screenshots',
specPattern: 'cypress/e2e/**/*.cy.ts',
setupNodeEvents(on, config) {
mkdirSync('results', { recursive: true });
on('before:run', (details) => {
console.log(`before:run: ${details.specs?.length} spec(s) in ${details.browser?.name} ${details.browser?.version}`);
});
on('after:spec', (spec, results) => {
const s = results.stats;
console.log(`after:spec: ${spec.relative} tests=${s.tests} passes=${s.passes} failures=${s.failures}`);
});
on('after:screenshot', (details) => {
console.log(`after:screenshot: ${details.name ?? '(failure)'} ${details.dimensions.width}x${details.dimensions.height} ${details.size} bytes`);
});
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron' && browser.isHeadless) {
launchOptions.args.push('--window-size=1400,1200', '--force-device-scale-factor=1');
}
return launchOptions;
});
const attempts = new Map<string, number>();
on('task', {
nextAttempt(key: string) {
const n = (attempts.get(key) ?? 0) + 1;
attempts.set(key, n);
return n;
},
log(message: string) {
appendFileSync('results/task.log', `${message}\n`);
console.log(`task log: ${message}`);
return null;
},
forgetful() {
// Returns undefined on purpose: cy.task() fails for this handler.
},
async seedBooks(titles: string[]) {
const res = await fetch(`${config.baseUrl}/api/test/seed`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ books: titles.map((title) => ({ title, author: 'Task Author' })) }),
});
return res.json();
},
});
return config;
},
},
});
Source: Cypress configuration
The runs in this page were started like this (the server was a separate Bookshelf process on its own port):
BASE_URL=http://localhost:3926 npx cypress run --browser chrome
Source: Cypress command line
Electron still works as a test browser in Cypress 16.2.0, but the run header now prints Electron 146 (headless) (deprecated)
and a warning that it will be removed in a future version, so the examples use Chrome unless a line says otherwise.
Debugging commands
Cypress commands do not run when the line that calls them executes. Each cy.* call only enqueues a command, and the queue runs
afterwards (see Core Concepts). That is why a bare debugger statement in the
body of a test stops before the first command has run. Cypress gives you three ways to stop at the right moment.
.debug()
.debug() is a query that you chain after any command. When the browser’s Developer Tools are open, it hits a breakpoint at
that point of the chain, exposes the yielded value to the console as a variable named subject and prints the subject and its
details to the console. It yields the same subject it received, so it can be left in the middle of a chain, and it never
adds an assertion.
-
Options: only
log(defaulttrue), which controls whether the command appears in the Command Log. -
The breakpoint only fires when Developer Tools are open. Under
cypress run(and with DevTools closed incypress open) the command is a no-op apart from its log line, so a forgotten.debug()does not hang a CI run. -
Because it is a query (since Cypress 12.0.0), Cypress can run it again while it retries the commands before it; it logs and hits the breakpoint only on the first run, so the subject you see can differ from the final one. It can no longer be overridden with
Cypress.Commands.overwrite().
.pause()
.pause() stops the command queue and leaves the application interactive. In the Command Log header you then choose
"Resume" to continue the whole test or "Next" to run only the next command and pause again. Unlike .debug() it does not set a
breakpoint in your code, and it works without DevTools. It yields the same subject it receives, but you should not chain
element commands after it, because the element may have changed while the test was paused. Options: log only.
Both commands can start a chain with cy (cy.pause(), cy.debug()), or follow another command.
describe('debugging commands', () => {
beforeEach(() => {
cy.request('POST', '/api/test/reset');
});
it('uses .debug() and .pause() without breaking the chain', () => {
cy.visit('/books');
cy.get('[data-testid=book-card]').debug().should('have.length', 6);
cy.pause();
cy.get('[data-testid=search-input]').type('quiet{enter}');
cy.get('[data-testid=book-card]').should('have.length', 1);
});
it('inspects variables with debugger inside then()', () => {
cy.visit('/books');
cy.get('[data-testid=book-card]').then(($cards) => {
const count = $cards.length;
// debugger // uncomment while DevTools is open
expect(count).to.eq(6);
});
});
});
The spec above passed in headless Chrome (2 passing): under cypress run the pause is skipped and the debug breakpoint does
not fire. The interactive behaviour (the breakpoint, the Resume and Next buttons) was not run here, because it needs
cypress open and a display.
The debugger statement
To inspect variables of the test itself, put debugger inside a .then() callback. A debugger placed directly in the test body
runs at the time the test function runs, before any command has run, and only variables that the callback actually references
are visible in the paused scope. In the spec above the commented line inside .then() shows the pattern: $cards, count
and everything else in scope are available when the pause hits with DevTools open.
Time travel and the Command Log
cypress open records a snapshot of the DOM for every command, and the Command Log lists them in order, including the commands of
before, beforeEach, afterEach and after hooks.
| Feature | What it does |
|---|---|
Hover |
Hovering over a command restores the application to the state it had when that command resolved and restores the URL of that
moment. For |
Pin |
Clicking a command pins its snapshot, so hovering other commands no longer changes the preview and you can inspect the DOM
with DevTools. Action commands such as |
Console output |
Clicking a command prints its details to the DevTools console: the command and its arguments, the subject, the yielded value, the elements found and the selector used. Elements printed there can be revealed in the Elements panel. |
Page events |
XHR and fetch requests, URL hash changes, page loads and form submissions are logged. HTTP request entries can be hidden without changing how the test behaves. |
Instrument panel |
When a test uses |
Keyboard shortcuts |
|
Snapshots cost memory. By default Cypress keeps the snapshots of the 50 most recent tests in cypress open; if a long spec makes
the browser use too much memory, lower numTestsKeptInMemory. Since Cypress 16 the manageBrowserMemory option (on by default)
collects garbage between tests automatically; the older experimentalMemoryManagement flag was removed in the same release.
None of this is available in cypress run, which keeps no Command Log UI. For failures that happen only in a pipeline, rely on
the screenshots Cypress takes on failure, the reporter output and, with Cypress Cloud, Test Replay.
Errors, source maps and debug logs
A failing test shows the error name (AssertionError, CypressError), the message with an optional "Learn more" link, a code
frame with the failing line and column that opens in your editor, a stack trace toggle and a "Print to console" button. The code
frame depends on source maps. Cypress adds inline source maps to spec files by default; if you replace the preprocessor, keep
inline maps on (devtool: 'inline-source-map' for webpack, sourcemap: 'inline' for esbuild, and sourceMap set to true in
tsconfig.json, not inlineSourceMap).
When the problem is Cypress itself (a browser that will not launch, a plugin that does not load), turn on its debug logging with
the DEBUG environment variable. The output is large, so narrow it to a namespace:
DEBUG=cypress:cli BASE_URL=http://localhost:3926 npx cypress run --browser chrome --spec cypress/e2e/screenshot.cy.ts
Source: Print DEBUG logs
The command printed lines such as these before the run:
cypress:cli parsed cli options { browser: 'chrome', spec: 'cypress/e2e/screenshot.cy.ts' }
cypress:cli verifying Cypress app with options {}
cypress:cli checking if executable exists /Users/<user>/Library/Caches/Cypress/16.2.0/Cypress.app/Contents/MacOS/Cypress
cypress:cli Binary is executable? : true
Source: Print DEBUG logs
Other values that help: DEBUG=cypress:* for everything, DEBUG=cypress:server*,-cypress:server:browsers* to exclude a
namespace, DEBUG=cypress:launcher:* for browser launch problems and DEBUG_DEPTH=3 to print nested objects that would
otherwise show as [Object]. In cypress open you can also set localStorage.debug = 'cypress*' in the DevTools console of the
runner and reload to see the driver logs. npx cypress info lists the browsers Cypress detected, and npx cypress cache path
and npx cypress cache list show where the binary lives and which versions are installed; on the test machine they printed
/Users/<user>/Library/Caches/Cypress and a single 16.2.0 entry.
Debugging flaky tests
Most flake comes from a test that moves on before the application is ready. Assert on as many intermediate steps as you can, wait
for aliased network requests before looking for the elements that depend on them, compare the local and CI environments, and
use Test Replay in Cypress Cloud to watch a CI failure as it happened. Fixed cy.wait(3000) calls hide timing problems instead of
removing them.
Cypress 16.2 also documents a terminal route for coding agents: npx cypress tap run <spec>, npx cypress tap status --json and
npx cypress tap reporter --test-id <id> run a spec while cypress open is running in another terminal and read back the
failures. These commands were not run here. They are covered with the other AI features in
AI Features in Cypress & Selenium.
Cypress Studio
Cypress Studio records interactions into a test. It is in beta, and the current version is Studio AI, which also suggests
assertions. Studio was not run for this page because it needs cypress open and a person clicking in a window.
-
Open it from the Cypress app: click "New Test" on a spec or suite, hover over a test in the Command Log and click "Edit in Studio", or click the "Studio Beta" button in the header. There is no configuration flag to set; the
experimentalStudiooption was removed in 15.4.0. -
It turns clicks and typing into
.click(),.type(),.check(),.uncheck()and.select()commands appended to the end of the test. You can pause recording to use DevTools. -
Selectors are chosen by priority:
data-cy,data-test,data-testid,data-qa,name,id,class, tag, other attributes, thennth-child.Cypress.ElementSelectorchanges the order. Bookshelf’sdata-testidattributes are therefore picked automatically (see Test Design & Page Objects). -
AI recommendations compare the DOM between interactions and add an assertion with an explanatory comment. They are written to the test immediately so you can run them before accepting, and you can accept or reject each one or all together. Values of password, credit card and hidden fields are removed before the DOM is sent to the model. They require a free Cypress Cloud account and a linked project (an account-less trial allows 6 recommendations), at least Cypress 15.11.0, and are limited per user and hour (60 on a free Cloud account, 300 on a paid account or trial). An organisation admin can switch AI features off in Cloud settings.
-
Limitations: end-to-end tests only (no component tests, no Cucumber), no recording across several origins, no iframes or Shadow DOM, recording is disabled while a test is failing, and the app needs source maps.
Treat the output as a first draft: the generated test uses the selectors and assertions you would otherwise write, so refactor it into the page objects and custom commands of the project afterwards.
Plugins and Node events
Cypress runs your spec files in the browser, but a project also has a Node.js side that can read files, query a database, call
other servers and react to what Cypress is doing. That side is the setupNodeEvents(on, config) function of the e2e (or
component) block in cypress.config.ts. Code there runs in Node, can use the file system, network and environment variables, and
cannot use cy or Cypress. Code in the support file runs in the browser next to the tests.
Plugins
A plugin is an npm package, installed as a dev dependency, that registers behaviour in one or both places:
-
Node-side (preprocessors, tasks, browser launch handling): register it in
setupNodeEventsand alwaysreturn config, because returning nothing silently discards the plugin’s configuration changes. -
Browser-side (custom commands, assertion libraries, overrides): import it in the support file.
-
Both, for plugins with a Node and a browser part such as
@cypress/grepand@cypress/code-coverage.
The curated list is at https://docs.cypress.io/app/plugins/plugins-list (visual testing, accessibility, code coverage, reporters,
grep and more). Check the "supported Cypress versions" badge before installing: Cypress 16 changes (for example
Cypress.env() and cy.exec() being removed) break plugins that have not been updated, and the plugin guide treats that as a
plugin issue even though the upgrade exposed it.
Troubleshooting from the guide: a cy is not defined error inside setupNodeEvents means browser code was registered in Node;
return config if the plugin changes configuration; restart cypress open after registering a plugin; and, to decide whether a
problem is yours or the plugin’s, disable the plugin’s registrations and rerun.
The Node events
| Event | Fires | Typical use |
|---|---|---|
|
Before a |
Log or prepare the environment. The handler gets |
|
Before each spec file. |
Per-spec setup, such as clearing a results folder. |
|
After each spec. The handler gets |
Delete the video of a passing spec, send a per-spec summary. |
|
After the whole run. |
Merge reports, upload artefacts, print totals. |
|
After a screenshot is written, from |
Rename or process the image. Return |
|
Before each browser starts. |
Change launch |
|
When a spec or support file has to be transpiled for the browser. |
Replace the bundler (for example esbuild or webpack). |
|
When a spec calls |
Run Node code from a test. |
The after:screenshot details object has size (bytes), takenAt, duration, dimensions (width, height), multipart,
pixelRatio, name, specName, path, scaled and blackout.
before:browser:launch receives browser (with name, family, version, isHeadless and isHeaded) and launchOptions, and
the object you return becomes the options:
| Option | Meaning |
|---|---|
|
Command-line switches pushed onto the array. No effect on Electron. |
|
Browser-specific settings. Chromium uses |
|
Absolute paths of unpacked extension folders. Headless Chrome cannot load extensions, Chrome-branded builds 137 and later no longer accept them this way (use Chrome for Testing or Chromium), and Electron loads only Chrome DevTools extensions. |
|
Environment variables for the browser process. Not supported on Electron. |
Headless browsers default to a 1280x720 screen, which sets the size of screenshots and videos but not viewportWidth and
viewportHeight. The registration in the example project makes headless Chromium-family browsers larger and removes device-scale
differences:
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron' && browser.isHeadless) {
launchOptions.args.push('--window-size=1400,1200', '--force-device-scale-factor=1');
}
return launchOptions;
});
Source: Browser Launch API
Running the whole example project in Chrome printed the events in order. The before:run line, the after:screenshot lines (note
the 1400 pixel width set above) and the after:spec lines come from the handlers in the configuration:
before:run: 5 spec(s) in chrome 155.0.8059.85
Running: flaky.cy.ts (2 of 5)
after:screenshot: (failure) 1400x1113 102236 bytes
after:spec: cypress/e2e/flaky.cy.ts tests=1 passes=1 failures=0
Running: screenshot.cy.ts (4 of 5)
after:screenshot: home-page 1000x787 76804 bytes
after:spec: cypress/e2e/screenshot.cy.ts tests=1 passes=1 failures=0
Source: Node Events Overview
Two details are worth knowing. A failure screenshot is taken at the full size of the headless window, whereas an explicit
cy.screenshot() is taken at the size of the application’s viewport (1000 by 787 above). In Electron the same screenshots were
2560 by 1440 for failures, so the before:browser:launch arguments only apply to non-Electron browsers. And a screenshot of a
failed attempt that is retried is named with (failed) and an (attempt n) suffix, as shown in the retries section.
In cypress open, before:run, after:run, before:spec and after:spec do not fire unless you set
experimentalInteractiveRunEvents: true.
cy.task
cy.task(event, arg, options) calls the handler registered under event in the task Node event and yields its return value. Use
it for anything the browser cannot do: seeding a database, reading and writing files outside the project’s fixtures, starting a helper
process or waiting for an external event. Cypress 16 removed cy.exec(), so a task is also how a spec runs a shell command.
| Rule | Detail |
|---|---|
Return value |
A handler returns a value or a promise. Returning |
Arguments |
The argument crosses into Node as JSON. Functions, symbols and |
Timeout |
|
Assertions |
Chained assertions run once and are not retried, because a task is not a query. |
Errors |
A thrown error or a rejected promise fails the test with its message. |
Options |
|
The example project registers four handlers (log, seedBooks, nextAttempt and forgetful, shown in the configuration above).
This spec exercises them, including the two ways a task call can fail:
describe('cy.task', () => {
it('seeds the catalogue from Node', () => {
cy.task('seedBooks', ['Debugging Dragons', 'Reporter Rabbits']).should('deep.equal', { books: 2, reviews: 3 });
cy.visit('/books');
cy.get('[data-testid=book-card]').should('have.length', 2);
cy.contains('Debugging Dragons').should('be.visible');
cy.task('log', 'seeded two books');
});
it('fails for an unregistered task', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.contain('was not handled in the setupNodeEvents method');
done();
});
cy.task('nothingRegistered');
});
it('fails when a handler returns undefined', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.contain('undefined');
done();
});
cy.task('forgetful');
});
});
Source: cy.task()
All three tests passed in headless Chrome (3 passing), and the log handler wrote seeded two books to the terminal and to
results/task.log. The error for the unregistered task reads, in part, The task 'nothingRegistered' was not handled in the
setupNodeEvents method. The following tasks are registered: log, seedBooks followed by the path of the configuration file, which
makes a mistyped task name easy to spot.
Reach for cy.request when the thing you need is an HTTP call that the page itself could also make; a task is the right tool when
the Node process needs to do the work, as seedBooks does here. A task that only forwards one HTTP call to the application
duplicates cy.request('POST', '/api/test/seed', ...) and was written this way only to show the mechanism.
Reporters
Cypress is built on Mocha, so any Mocha reporter works. The default is spec, which prints each test and its result to the
terminal. Cypress ships junit and teamcity as ready-to-use reporters, and also accepts any other Mocha reporter, either a
package name or a path to a file.
npx cypress run --reporter junit --reporter-options "mochaFile=results/my-test-output.xml,toConsole=true"
npx cypress run --reporter reporters/custom.js
npx cypress run --reporter mochawesome
Source: Reporters
The same options can be set in the configuration with the reporter and reporterOptions keys, as in the example project:
reporter: 'junit',
reporterOptions: {
mochaFile: 'results/junit-[hash].xml',
toConsole: false,
},
Source: Reporter options
Each spec file is run and reported separately, so a fixed mochaFile is overwritten by every spec and only the last one survives.
The [hash] token gives each spec its own file; the run above wrote five files for five specs:
<testsuites name="Mocha Tests" time="0.394" tests="2" failures="0">
<testcase name="debugging commands uses .debug() and .pause() without breaking the chain" time="0.303" classname="uses .debug() and .pause() without breaking the chain">
</testcase>
<testcase name="debugging commands inspects variables with debugger inside then()" time="0.060" classname="inspects variables with debugger inside then()">
</testcase>
</testsuites>
Merge the files in an after:run handler or in a pipeline step afterwards (junit-report-merger for JUnit XML,
mochawesome-merge for Mochawesome JSON).
| Goal | Setup |
|---|---|
Live terminal output and a machine-readable file |
|
A browsable HTML report |
|
Teamcity service messages |
The built-in |
Cypress Cloud |
Independent of the reporter: recording a run to Cloud does not need one (see Cypress Cloud). |
The multi-reporter, Mochawesome and TeamCity set-ups were not run here; only the spec and junit reporters were. Clear the
results folder before each run, because stale files from a previous run are merged along with the new ones.
Code coverage
Cypress does not instrument your code. Coverage is a three-step arrangement: instrument the application with Istanbul, let the
@cypress/code-coverage plugin collect window.__coverage__ after each test, and let it merge the data and write the reports
with nyc.
-
Instrument the code. Options:
nyc instrumentover the source folder,babel-plugin-istanbulin the Babel config (scope it to a Babel environment such ascypressand setBABEL_ENV=cypressin the Cypress scripts to avoid a duplicate-plugin error with Jest), orvite-plugin-istanbulfor Vite projects. Source maps are kept, so DevTools shows the original code. Code innode_modulesis skipped. -
Install
@cypress/code-coverage, import@cypress/code-coverage/supportin the support file, and register the tasks insetupNodeEvents:import { defineConfig } from 'cypress'; import codeCoverageTask from '@cypress/code-coverage/task'; export default defineConfig({ e2e: { setupNodeEvents(on, config) { codeCoverageTask(on, config); return config; }, }, });
Source: Code coverage
Then run the tests. The merged data is saved in .nyc_output and an HTML report is generated in coverage/index.html;
npx nyc report produces other formats such as a text summary.
Further points from the guide:
-
Component tests need the same support import in the component support file; an end-to-end-only import collects nothing for them. Vite projects reuse their Vite configuration, webpack projects need
babel-plugin-istanbulin the dev server pipeline. -
Pure functions can be imported into a spec and tested directly. To count those specs, use the
babelrcpreprocessor from@cypress/code-coverage/use-babelrcon thefile:preprocessorevent; their coverage is merged with the end-to-end coverage. -
Full-stack coverage: run the Node server under
nyc, expose aGET /__coverage__route (or the plugin’s Express or hapi middleware) that returnsglobal.__coverage__, and setenv.codeCoverage.urlin the Cypress configuration to that endpoint. The plugin then merges backend and frontend coverage. Register the route only when coverage is enabled and exclude it from coverage. -
Parallel runs produce partial reports; copy each machine’s coverage into one folder with unique names and combine them with
nyc merge. -
A covered line only proves the line ran, not that an assertion checks it. Coverage is a guide to missing tests, and it complements UI Coverage (which tracks interactive elements without instrumentation) and visual testing.
This section was not run. Bookshelf’s front end is plain JavaScript served by the server without a build step, so there is no bundler to attach Istanbul to, and the example would have needed a different application. It follows the official guide.
Test retries
Retries are off by default: a failing test fails. Turn them on with the retries option, either as a number for both modes or as
an object with runMode (for cypress run) and openMode (for cypress open). Both default to 0, and retries: 2 allows two
extra attempts, three in total. The example project uses retries: { runMode: 1, openMode: 0 }, so CI retries once while the
interactive runner does not hide a failure.
The same object can be given to one test or to a suite, which overrides the global value:
describe('retries', () => {
beforeEach(() => {
cy.request('POST', '/api/test/reset');
});
it('passes on the second attempt', { retries: { runMode: 2, openMode: 0 } }, () => {
cy.log(`attempt index ${Cypress.currentRetry}`);
cy.visit('/books');
// Fails on attempt 0 on purpose, then passes on the retry.
cy.get('[data-testid=book-card]').should('have.length', Cypress.currentRetry === 0 ? 7 : 6);
});
});
Source: Test retries
The first attempt of this test fails on purpose, because Cypress.currentRetry is 0 and the test expects seven cards. The run
reported the test as passed and kept the screenshot of the failed attempt:
Running: retries.cy.ts (3 of 5)
after:screenshot: (failure) 1400x1113 101230 bytes
after:spec: cypress/e2e/retries.cy.ts tests=1 passes=1 failures=0
...
ries.cy.ts/retries -- passes on the second attempt (failed).png
Source: Test retries
Behaviour to know:
-
beforeEachandafterEachhooks run again with each attempt, which is why the example resets the application inbeforeEach. A failure inside abeforeorafterhook does not trigger a retry. -
Screenshots of each attempt get an
(attempt n)suffix, and a failed attempt also gets(failed). A retried test that finally passes still leaves the screenshot of the earlier failure, so the screenshots folder can contain pictures for a green run. -
Retries do not add test results for billing. A test counts once however many attempts it takes.
-
Cypress.currentRetryis the index of the current attempt (0 for the first run). -
In
cypress openthe Command Log shows each attempt, expandable for review. -
The
after:specresults.tests[].attemptsarray lets a handler find tests that needed a retry, for example to keep the video of a spec only if it had a failed attempt. -
In Cypress Cloud, tests that failed and then passed are marked flaky and filterable, and the flake rate is tracked per test.
Experimental retry strategies
Since Cypress 13.4.0 the retries.experimentalStrategy and retries.experimentalOptions options let you decide when a test with
failures counts as a pass, so that flake is detected rather than hidden. They are global (a per-test retries setting overrides
them), and runMode and openMode must then be booleans.
| Strategy and options | Outcome |
|---|---|
|
The test passes when it reaches |
|
Any test that failed at least once ends as failed, but is marked flaky if a retry passed, and retries stop early when
|
import base from './cypress.config';
import { defineConfig } from 'cypress';
export default defineConfig({
...base,
retries: {
experimentalStrategy: 'detect-flake-and-pass-on-threshold',
experimentalOptions: { maxRetries: 2, passesRequired: 2 },
openMode: false,
runMode: true,
},
});
Source: Experimental test retries
// Fails on the first attempt only, so the test is flaky but eventually passes.
it('is flaky on purpose', () => {
cy.request('POST', '/api/test/reset');
cy.task<number>('nextAttempt', 'flaky').then((attempt) => {
cy.log(`attempt ${attempt}, Cypress.currentRetry ${Cypress.currentRetry}`);
cy.visit('/books');
cy.get('[data-testid=book-card]').should('have.length', attempt === 1 ? 7 : 6);
});
});
Source: cy.task()
The test fails on its first attempt only; the Node-side counter from the nextAttempt task decides which attempt it is. Run with --config-file cypress.flake.config.ts in Chrome, the
spec ended 1 passing with one failure screenshot, which matches fail, pass, pass. The terminal output does not print a flaky
badge; the flaky classification is shown in Cypress Cloud, which was not available for this run.
Test performance
The official performance guide is about a suite that is slow because of what it tests and how, more than about Cypress itself. Its recommendations, in order of effect:
-
Test at the lowest level that proves the behaviour. Cover rules with API tests (
cy.request) and component tests, and keep end-to-end tests for the critical journeys. A typical split is 30 to 40 percent API, 30 to 40 percent component and 20 to 30 percent end to end. -
Cache login with
cy.session()and arrange data through the API or a task, never through the UI (see Sessions, Authentication & Cross-Origin and theseedBookstask above). -
Stub responses in tests that are about the interface, and avoid broad
cy.intercept('*')handlers. -
Block third-party hosts that do nothing for the test with
blockHosts, for exampleblockHosts: ['*google-analytics.com', '*sentry.io']. -
Use specific selectors and wait on conditions, not time:
cy.get('[data-testid=toast]').should('be.visible')rather thancy.wait(3000). -
Keep imports lean: avoid barrel files in the support file and move Node-only code into tasks.
-
Cypress 16 uses the modern visibility algorithm by default;
visibilityStrategy: 'legacy'is only a temporary migration aid. -
Retry only where needed (
retries: { runMode: 1, openMode: 0 }). -
Parallelise by spec file (
cypress run --record --key <key> --parallel, which needs Cypress Cloud) and balance spec durations: under one minute is excellent, one to three acceptable, over five minutes should be split, and splitting below ten seconds rarely pays. Cloud’s spec prioritisation (previous failures first) and auto cancellation after a number of failures are configured in Cloud. -
Set
video: falseunless you need it (video is off by default since Cypress 13, and Test Replay is a smaller alternative) and leavemanageBrowserMemoryon. -
Tier your runs with
@cypress/greptags, for examplenpx cypress run --expose grepTags="@smoke @critical",grepFilterSpecs=true,grepOmitFiltered=true, but keep at least one full run for Cloud comparisons. -
Cache the Cypress binary in CI (
~/.cache/Cypresson Linux,~/Library/Caches/Cypresson macOS, keyed on the lock file) and do not cachenode_modulesitself. Give the CI job enough CPU, and profile it withDEBUG=cypress:server:util:process_profiler npx cypress runif the CPU stays near 100 percent.
Source: Test performance
Measured on the example project, the five specs (eight tests, including two that fail once and retry) finished in 9 seconds in Chrome, of which the two retried tests account for about 8, because a failed assertion waits the 4 second default timeout before it is reported. That is a reminder that the cheapest speed-up is an assertion that passes the first time.
What changed recently
-
16.2.0 (13 October 2026): adds
cy.getByRole(); the bundled Electron is 146, shown as deprecated in the run header. -
16.0.0 (1 September 2026):
Cypress.env()andcy.exec()removed (usecy.env()/Cypress.expose()andcy.task()),experimentalMemoryManagementreplaced by the defaultmanageBrowserMemory, modern visibility algorithm by default, native network interception on Chromium browsers; Node 20 and 25 are no longer supported. -
15.11.0: Studio AI recommendations require this version or later. 15.4.0:
experimentalStudioremoved. -
13.4.0:
retries.experimentalStrategyandretries.experimentalOptionsintroduced. -
12.0.0:
.debug()became a query that cannot be overridden. -
7.1.0:
experimentalInteractiveRunEventsadded for running the run and spec events incypress open. -
5.6.0: type parameter for the value yielded by
cy.task.