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
| Change | Action needed |
|---|---|
| Native browser network in Chrome, Chromium, and Edge | Usually none. Update tests that assert on req.httpVersion, compression headers, or a 304 status. |
| Modern visibility algorithm on by default | Usually none. Update tests that relied on legacy-only visibility semantics. |
| Browser memory management on by default | Only if you set experimentalMemoryManagement: false. Replace it with manageBrowserMemory: false. |
keystrokeDelay default changed from 10 to 0 | Only if a test depended on the implicit delay. |
| Cookie and storage reads are now queries | Usually none. Move assertions out of .then() and onto .should() to get retries. |
| Node.js 20 and 25 dropped | Yes, if you run Cypress on either version. |
Cypress.env() removed | Yes. Migrate to cy.env() or Cypress.expose(). |
cy.end() removed | Yes. Delete the .end() calls. |
cy.exec() removed | Yes. Replace the calls with cy.task(). |
viewportWidth, viewportHeight, and blockHosts via Cypress.config() | Yes, if you set them while a test is executing. |
experimentalSourceRewriting removed | Yes. Remove the option, and enable removeSRIAttributes if you used it for SRI. |
| CoffeeScript support removed | Yes, if you have .coffee specs, support files, or fixtures. |
| Angular, Vite, and Next.js minimums raised for component testing | Yes, if you are below Angular 21, Vite 8, or Next.js 15.0.4. |
| Electron deprecated as a test browser | Not 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:
- cypress.config.js
- cypress.config.ts
const { defineConfig } = require('cypress')
module.exports = defineConfig({
forceHttp1: true,
})
import { defineConfig } from 'cypress'
export default 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.
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​
- cypress.config.js
- cypress.config.ts
const { defineConfig } = require('cypress')
module.exports = defineConfig({
keystrokeDelay: 10,
})
import { defineConfig } from 'cypress'
export default 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:
- cypress.config.js
- cypress.config.ts
const { defineConfig } = require('cypress')
module.exports = defineConfig({
visibilityStrategy: 'legacy',
})
import { defineConfig } from 'cypress'
export default defineConfig({
visibilityStrategy: 'legacy',
})
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.
{
experimentalMemoryManagement: true,
}
{
// 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.
{
experimentalMemoryManagement: false,
}
{
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:
- Query commands (
cy.getCookie(),cy.getCookies(), andcy.getAllCookies()) are now governed bydefaultCommandTimeout(default4000ms), the same timeout used by every other query. - Cookie action commands (
cy.setCookie(),cy.clearCookie(),cy.clearCookies(), andcy.clearAllCookies()) are unchanged. They make a single automation round-trip, do not retry assertions, and remain governed byresponseTimeout(default30000ms).
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:
// 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')
})
// 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.
Cypress.Commands.overwrite('getCookie', (name, options) => {
return cy.getCookie(name, options)
})
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:
// 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')
})
// 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.
Cypress.Commands.overwrite('getAllLocalStorage', (options) => {
return cy.getAllLocalStorage(options)
})
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.
it('does not display the sidebar on small screens', () => {
Cypress.config('viewportWidth', 400)
Cypress.config('viewportHeight', 1000)
cy.get('#sidebar').should('not.be.visible')
})
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.