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;
    },
  },
});

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

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 (default true), 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 in cypress 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);
    });
  });
});

Source: .debug(), .pause()

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 cy.contains() the matching element is highlighted and scrolled into view.

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 .click() show a red hitbox where the event happened and often keep a "before" and an "after" snapshot you can switch between.

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 cy.intercept(), cy.stub() or cy.spy(), a panel summarises the routes, stubs and spies and how often they were called.

Keyboard shortcuts

r reruns the tests, s stops them and f focuses the spec list.

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

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

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 experimentalStudio option 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, then nth-child. Cypress.ElementSelector changes the order. Bookshelf’s data-testid attributes 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 setupNodeEvents and always return 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/grep and @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:run

Before a cypress run starts, once per machine in parallel runs. In cypress open only with experimentalInteractiveRunEvents.

Log or prepare the environment. The handler gets details with config, browser and specs (undefined when interactive), system, cypressVersion, specPattern and the Cloud fields (parallel, group, runUrl, tag). Return a promise and Cypress waits for it.

before:spec

Before each spec file.

Per-spec setup, such as clearing a results folder.

after:spec

After each spec. The handler gets spec and results (stats, reporter, tests with attempts, error, video, screenshots); results is undefined under cypress open.

Delete the video of a passing spec, send a per-spec summary.

after:run

After the whole run.

Merge reports, upload artefacts, print totals.

after:screenshot

After a screenshot is written, from cy.screenshot() or a test failure.

Rename or process the image. Return path, size or dimensions and Cypress merges them; other fields are ignored.

before:browser:launch

Before each browser starts.

Change launch args, preferences, extensions and env.

file:preprocessor

When a spec or support file has to be transpiled for the browser.

Replace the bundler (for example esbuild or webpack).

task

When a spec calls cy.task().

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

args

Command-line switches pushed onto the array. No effect on Electron.

preferences

Browser-specific settings. Chromium uses preferences.default (and localState, secureDefault); Firefox takes a flat map of names to values; Electron takes BrowserWindow options such as darkTheme.

extensions

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.

env

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;
});

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

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 undefined (or resolving with it) fails the command; return null when there is nothing to send back.

Arguments

The argument crosses into Node as JSON. Functions, symbols and undefined properties are dropped, RegExp, Map and Set become empty objects, dates become ISO strings and circular structures make the command fail.

Timeout

taskTimeout (60 seconds) by default; override with the timeout option, per suite or test, or with Cypress.config('taskTimeout', 30000). Long-running processes such as servers or file watchers are not supported.

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

log (default true) and timeout.

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,
},

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

cypress-multi-reporters with a reporterEnabled list such as spec, mocha-junit-reporter in a JSON file and mochaFile: "cypress/results/results-[hash].xml". Install cypress-multi-reporters and mocha-junit-reporter as dev dependencies; the multi-reporter replaces the built-in junit reporter.

A browsable HTML report

mochawesome with reportDir, overwrite: false, html: false and json: true to write one JSON file per spec; then npx mochawesome-merge cypress/results/*.json -o mochawesome.json and npx marge mochawesome.json, which writes mochawesome-report/mochawesome.html.

Teamcity service messages

The built-in teamcity reporter.

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.

  1. Instrument the code. Options: nyc instrument over the source folder, babel-plugin-istanbul in the Babel config (scope it to a Babel environment such as cypress and set BABEL_ENV=cypress in the Cypress scripts to avoid a duplicate-plugin error with Jest), or vite-plugin-istanbul for Vite projects. Source maps are kept, so DevTools shows the original code. Code in node_modules is skipped.

  2. Install @cypress/code-coverage, import @cypress/code-coverage/support in the support file, and register the tasks in setupNodeEvents:

    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-istanbul in the dev server pipeline.

  • Pure functions can be imported into a spec and tested directly. To count those specs, use the babelrc preprocessor from @cypress/code-coverage/use-babelrc on the file:preprocessor event; their coverage is merged with the end-to-end coverage.

  • Full-stack coverage: run the Node server under nyc, expose a GET /__coverage__ route (or the plugin’s Express or hapi middleware) that returns global.__coverage__, and set env.codeCoverage.url in 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:

  • beforeEach and afterEach hooks run again with each attempt, which is why the example resets the application in beforeEach. A failure inside a before or after hook 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.currentRetry is the index of the current attempt (0 for the first run).

  • In cypress open the Command Log shows each attempt, expandable for review.

  • The after:spec results.tests[].attempts array 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

detect-flake-and-pass-on-threshold with maxRetries and passesRequired

The test passes when it reaches passesRequired passing attempts within maxRetries retries. With maxRetries: 2 and passesRequired: 2: fail, pass, pass is passing and flaky; fail, pass, fail is failing and flaky; fail, fail stops early and is failing and not flaky.

detect-flake-but-always-fail with maxRetries and stopIfAnyPassed

Any test that failed at least once ends as failed, but is marked flaky if a retry passed, and retries stop early when stopIfAnyPassed is true.

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,
  },
});
// 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:

  1. 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.

  2. Cache login with cy.session() and arrange data through the API or a task, never through the UI (see Sessions, Authentication & Cross-Origin and the seedBooks task above).

  3. Stub responses in tests that are about the interface, and avoid broad cy.intercept('*') handlers.

  4. Block third-party hosts that do nothing for the test with blockHosts, for example blockHosts: ['*google-analytics.com', '*sentry.io'].

  5. Use specific selectors and wait on conditions, not time: cy.get('[data-testid=toast]').should('be.visible') rather than cy.wait(3000).

  6. Keep imports lean: avoid barrel files in the support file and move Node-only code into tasks.

  7. Cypress 16 uses the modern visibility algorithm by default; visibilityStrategy: 'legacy' is only a temporary migration aid.

  8. Retry only where needed (retries: { runMode: 1, openMode: 0 }).

  9. 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.

  10. Set video: false unless you need it (video is off by default since Cypress 13, and Test Replay is a smaller alternative) and leave manageBrowserMemory on.

  11. Tier your runs with @cypress/grep tags, for example npx cypress run --expose grepTags="@smoke @critical",grepFilterSpecs=true,grepOmitFiltered=true, but keep at least one full run for Cloud comparisons.

  12. Cache the Cypress binary in CI (~/.cache/Cypress on Linux, ~/Library/Caches/Cypress on macOS, keyed on the lock file) and do not cache node_modules itself. Give the CI job enough CPU, and profile it with DEBUG=cypress:server:util:process_profiler npx cypress run if the CPU stays near 100 percent.

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() and cy.exec() removed (use cy.env()/Cypress.expose() and cy.task()), experimentalMemoryManagement replaced by the default manageBrowserMemory, 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: experimentalStudio removed.

  • 13.4.0: retries.experimentalStrategy and retries.experimentalOptions introduced.

  • 12.0.0: .debug() became a query that cannot be overridden.

  • 7.1.0: experimentalInteractiveRunEvents added for running the run and spec events in cypress open.

  • 5.6.0: type parameter for the value yielded by cy.task.