Skip to main content
Cypress App

Upgrade Cypress

Migrating to Cypress 16.0​

Upgrade to Cypress 16 with your AI assistant

Copies a ready-made prompt that walks your AI coding assistant through this migration. Works with any AI tool that can read and edit your project.

Read https://docs.cypress.io/app/references/migration-guide/migrating-to-cypress-16-0.md and https://docs.cypress.io/app/references/migration-guide/migrating-away-from-cypress-env.md, which covers in full the Cypress.env() removal that the 16.0 section only summarizes. Then upgrade this project from Cypress 15 to 16 by working through these steps: 1. Confirm the starting point. If the project isn't on Cypress 15.x yet, stop and tell me; majors should be upgraded one at a time. 2. Check requirements. Verify my environment meets the requirements in the 16.0 section (Node.js 22, 24, or 26+, and Angular 21+ / Vite 8+ / Next.js 15.0.4+ if I use component testing) and flag anything unsupported. 3. Update Cypress. Detect my package manager and update the cypress dependency. If I depend on @cypress/angular-zoneless, replace it with @cypress/angular and update the imports. 4. Migrate off Cypress.env(), following the Cypress.env() page rather than guessing. Find every Cypress.env() usage and classify each value as sensitive or public, since that choice decides the replacement. Move sensitive values (keys, tokens, passwords, credentials) to cy.env(), which is asynchronous and yields only the keys you request, and public values (feature flags, API versions, public URLs) to Cypress.expose() with a top-level expose block in my config. Replace any env key in a per-test or per-suite config override with expose. Check for --env CLI flags, for reads inside cy.origin() callbacks, and for plugins that call Cypress.env(), which that page lists with the versions that support the new APIs. Show me the proposed changes before applying them. 5. Fix the remaining breaking changes in the 16.0 section that affect my code and config: delete cy.end() calls, replace cy.exec() calls with cy.task() registered in setupNodeEvents and remove execTimeout, replace experimentalMemoryManagement: false with manageBrowserMemory: false, remove allowCypressEnv, experimentalSourceRewriting, and experimentalFastVisibility, move Cypress.config() calls that set viewportWidth, viewportHeight, or blockHosts during a test into cy.viewport() or a test configuration object, and convert any .coffee specs, support files, or fixtures to JavaScript or TypeScript. 6. Review what changed behavior without changing my code. Flag, but don't rewrite unless a test actually fails: assertions on req.httpVersion, compression headers, or a 304 status inside cy.intercept(); visibility assertions that depended on the legacy algorithm; tests that relied on the implicit 10ms keystrokeDelay; and cookie or storage assertions written inside .then(), which doesn't retry. Don't set forceHttp1 or visibilityStrategy: 'legacy' to make a test pass; both are deprecated escape hatches. 7. Verify. Run npx cypress verify and my Cypress tests, and confirm the run starts with no deprecation or removed-option warnings. Finish with a summary of what changed and anything you couldn't safely automate.

This guide details the code changes needed to migrate to Cypress version 16.0. See the full changelog for version v16.0.

What you may need to change

ChangeAction needed
Native browser network in Chrome, Chromium, and EdgeUsually none. Update tests that assert on req.httpVersion, compression headers, or a 304 status.
Modern visibility algorithm on by defaultUsually none. Update tests that relied on legacy-only visibility semantics.
Browser memory management on by defaultOnly if you set experimentalMemoryManagement: false. Replace it with manageBrowserMemory: false.
keystrokeDelay default changed from 10 to 0Only if a test depended on the implicit delay.
Cookie and storage reads are now queriesUsually none. Move assertions out of .then() and onto .should() to get retries.
Node.js 20 and 25 droppedYes, if you run Cypress on either version.
Cypress.env() removedYes. Migrate to cy.env() or Cypress.expose().
cy.end() removedYes. Delete the .end() calls.
cy.exec() removedYes. Replace the calls with cy.task().
viewportWidth, viewportHeight, and blockHosts via Cypress.config()Yes, if you set them while a test is executing.
experimentalSourceRewriting removedYes. Remove the option, and enable removeSRIAttributes if you used it for SRI.
CoffeeScript support removedYes, if you have .coffee specs, support files, or fixtures.
Angular, Vite, and Next.js minimums raised for component testingYes, if you are below Angular 21, Vite 8, or Next.js 15.0.4.
Electron deprecated as a test browserNot yet, but plan to switch to an installed browser.

Node.js 20 and 25 no longer supported​

Cypress 16 requires Node.js 22.x, 24.x, or 26.x and above to install the Cypress binary. Node.js 20 and Node.js 25 are no longer supported. See system requirements and Node's release schedule.

Chrome, Chromium, and Edge use the native browser network​

In Cypress 16, Chrome, Chromium, and Edge intercept test traffic through the native browser network. Your application connects to your server directly and negotiates whatever protocol the server supports (HTTP/1.1, HTTP/2, or HTTP/3), exactly as it does in production.

Firefox, WebKit, and Electron continue to use the legacy network path and are unaffected.

cy.intercept() works as before, and most suites need no edits. A few behaviors differ in Chrome, Chromium, and Edge on the native browser network. See Native network interception for the full list with examples.

Aside from the cy.intercept() behaviors listed in that guide, your suite should not need any changes. Commands, stubbing, waiting, aliases, and assertions all work as they did before, and adopting the native browser network requires no configuration change.

If you need a temporary escape hatch, either while you update those tests or if you hit behavior on the native browser network that the guide does not list, route every browser back through the legacy network path:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
forceHttp1: true,
})

If setting forceHttp1 to true allows your suite to pass, and the difference is not one of the documented behaviors, open an issue with what you are experiencing so it can be investigated.

caution

forceHttp1 is both introduced and deprecated in Cypress 16. It exists only to give suites time to migrate, or as a temporary escape hatch while an issue you have filed is fixed, and it will be removed once the native browser network is supported long term. Do not set it to true only to avoid updating tests for the native browser network, such as skipping Cypress.isBrowser() gates. Treat it as a temporary aid, not a permanent setting.

Default keystrokeDelay changed from 10 to 0​

The default delay between keystrokes in cy.type() has been changed from 10 to 0 milliseconds to improve performance of typing-heavy test runs.

If your tests relied on the implicit 10ms delay between keystrokes, you can restore the previous behavior using any of these approaches. The resolved keystrokeDelay is read from your Cypress configuration first, then Cypress.Keyboard.defaults(), and falls back to the built-in default of 0 if neither is set. A per-command delay passed to .type() is independent of this and overrides the resolved value for that single call.

Set keystrokeDelay in your Cypress configuration​

const { defineConfig } = require('cypress')

module.exports = defineConfig({
keystrokeDelay: 10,
})

Set keystrokeDelay via Cypress.Keyboard.defaults()​

Cypress.Keyboard.defaults({
keystrokeDelay: 10,
})

Set delay per command​

cy.get('input').type('slow typing', { delay: 10 })

experimentalFastVisibility replaced by visibilityStrategy​

The experimental experimentalFastVisibility boolean flag has been removed. Its successor is the new visibilityStrategy configuration option, which accepts 'legacy' or 'modern' and defaults to 'modern'. The modern algorithm is now the default for all users, not just those who opted into the experiment.

If you were setting experimentalFastVisibility: true: remove it. The modern algorithm is now the default.

If you were setting experimentalFastVisibility: false (or relying on the 15.x default) and a test now fails: the modern algorithm intentionally drops some behaviors the legacy algorithm had (ancestor overflow clipping, transform-based hiding, and coverage detection for fixed/sticky elements). Prefer rewriting the affected assertions to verify the same user-visible behavior in an algorithm-agnostic way. See Visibility Strategy for what each algorithm checks and recommended rewrite patterns.

If you need a temporary escape hatch while you update those tests, opt back into the legacy algorithm:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
visibilityStrategy: 'legacy',
})
caution

visibilityStrategy is deprecated. Both the 'legacy' value and the option itself will be removed in a future major version of Cypress. Treat it as a temporary migration aid only.

experimentalMemoryManagement replaced by manageBrowserMemory​

The experimental experimentalMemoryManagement configuration option has been replaced by the new manageBrowserMemory configuration option, which defaults to true. Browser memory management is now on by default for all users running Chromium-based browsers.

If your Cypress configuration still sets experimentalMemoryManagement, Cypress now ignores the option and prints a warning.

If you were setting experimentalMemoryManagement: true: remove it. Memory management is now the default.

Before
{
experimentalMemoryManagement: true,
}
After
{
// Remove the experimentalMemoryManagement option
}

If you were setting experimentalMemoryManagement: false: replace it with manageBrowserMemory: false. Deleting it without adding the replacement silently opts you into memory management.

Before
{
experimentalMemoryManagement: false,
}
After
{
manageBrowserMemory: false,
}

See manageBrowserMemory for more information.

cy.getCookie(), cy.getCookies(), and cy.getAllCookies() are now queries​

cy.getCookie(), cy.getCookies(), and cy.getAllCookies() are now retry-able query commands, just like cy.get() and cy.contains().

Previously they were one-shot commands that resolved a single value and ran any chained assertions only once. As of Cypress 16 they retry their built-in and chained assertions until they pass or time out.

Reads are now retried​

A read such as cy.getCookie('session_id').should('exist') no longer fails on the first missing read. The command re-reads the cookie and retries the assertion until it passes or the command times out, so you no longer need to manually wait for a cookie to be written before asserting on it.

Timeouts now follow defaultCommandTimeout​

This changes which timeout governs them:

The new retry behavior applies only to assertions chained directly onto the command. A .then() callback still runs only once, after the command settles, and is not retried. The cookie (or array of cookies) it receives is a single snapshot read at that moment, so an assertion made inside .then() will not wait for the cookie to be set. To benefit from retries, chain .should() assertions directly instead:

Before
// Runs once and does not retry, so it fails immediately if the cookie isn't set yet
cy.getCookie('session_id').then((cookie) => {
expect(cookie).to.have.property('value', '189jd09su')
})
After
// Retries, re-reading the cookie, until the assertion passes or times out
cy.getCookie('session_id').should('have.property', 'value', '189jd09su')

If you specifically want a one-shot check that does not retry, for example to assert that a cookie does not yet exist, perform the assertion inside a .then() callback, which runs once and is not retried:

cy.getCookie('session_id').then((cookie) => {
expect(cookie).to.be.null
})

cy.getCookie(), cy.getCookies(), and cy.getAllCookies() can no longer be overwritten with Cypress.Commands.overwrite()​

Queries must be overwritten using Cypress.Commands.overwriteQuery(). If you were previously overwriting cy.getCookie(), cy.getCookies(), or cy.getAllCookies(), update your code to use Cypress.Commands.overwriteQuery('getCookie', function () { ... }) rather than Cypress.Commands.overwrite('getCookie', () => { ... }). For more details on overwriting queries, see Overwriting Existing Queries.

Before
Cypress.Commands.overwrite('getCookie', (name, options) => {
return cy.getCookie(name, options)
})
After
Cypress.Commands.overwriteQuery('getCookie', (name, options) => {
return cy.getCookie(name, options)
})

See Overwriting Existing Queries for more details.

cy.getAllLocalStorage() and cy.getAllSessionStorage() are now queries​

cy.getAllLocalStorage() and cy.getAllSessionStorage() are now retry-able query commands, just like cy.getCookie() and cy.getCookies().

Previously they read storage once and ran any chained assertions a single time, so they could fail on the first read when storage was populated asynchronously. As of Cypress 16 they re-read localStorage and sessionStorage from all origins and retry their chained assertions until they pass or time out.

Reads are now retried​

A read such as cy.getAllLocalStorage().should('have.property', 'https://example.cypress.io') no longer fails on the first empty read. The command re-reads storage and retries the assertion until it passes or the command times out, so you no longer need to manually wait for storage to be written before asserting on it.

Timeouts now follow defaultCommandTimeout​

Both commands are now Timeoutable and accept a timeout option. They are governed by defaultCommandTimeout (default 4000 ms), the same timeout used by every other query. When a read times out, the error is reported with the standard Timed out retrying after ... prefix.

The new retry behavior applies only to assertions chained directly onto the command. A .then() callback still runs only once, after the command settles, and is not retried. The object it receives is a single snapshot read at that moment, so an assertion made inside .then() will not wait for storage to be populated. To benefit from retries, chain .should() assertions directly instead:

Before
// Runs once and does not retry, so it fails immediately if the origin isn't present
cy.getAllLocalStorage().then((result) => {
expect(result).to.have.property('https://example.cypress.io')
})
After
// Retries, re-reading storage, until the assertion passes or times out
cy.getAllLocalStorage().should('have.property', 'https://example.cypress.io')

If you specifically want a one-shot check that does not retry, for example to assert that no storage has been written yet, perform the assertion inside a .then() callback, which runs once and is not retried:

cy.getAllLocalStorage().then((result) => {
expect(result).to.deep.equal({})
})

cy.getAllLocalStorage() and cy.getAllSessionStorage() can no longer be overwritten with Cypress.Commands.overwrite()​

Queries must be overwritten using Cypress.Commands.overwriteQuery(). If you were previously overwriting cy.getAllLocalStorage() or cy.getAllSessionStorage(), update your code to use Cypress.Commands.overwriteQuery('getAllLocalStorage', function () { ... }) rather than Cypress.Commands.overwrite('getAllLocalStorage', () => { ... }). For more details on overwriting queries, see Overwriting Existing Queries.

Before
Cypress.Commands.overwrite('getAllLocalStorage', (options) => {
return cy.getAllLocalStorage(options)
})
After
Cypress.Commands.overwriteQuery('getAllLocalStorage', (options) => {
return cy.getAllLocalStorage(options)
})

See Overwriting Existing Queries for more details.

viewportWidth and viewportHeight can no longer be set with Cypress.config() during test execution​

viewportWidth and viewportHeight can no longer be set with Cypress.config() while a test is executing. Doing so applied to the next test rather than the current one, and the change was not reflected in Test Replay, so Cypress now throws instead.

Use cy.viewport() to change the viewport during a test, or set the values in the test configuration of a describe, context, or it block.

Before
it('does not display the sidebar on small screens', () => {
Cypress.config('viewportWidth', 400)
Cypress.config('viewportHeight', 1000)

cy.get('#sidebar').should('not.be.visible')
})
After
it('does not display the sidebar on small screens', () => {
cy.viewport(400, 1000)

cy.get('#sidebar').should('not.be.visible')
})

Or, to apply the viewport to a whole suite or test:

describe(
'page display on medium size screen',
{ viewportWidth: 400, viewportHeight: 1000 },
() => {
it('does not display the sidebar', () => {
cy.