---
id: app/releases/migration-guide#migrating-to-cypress-16-0
title: Migrating to Cypress 16.0
page_title: 'Upgrade Cypress: version migration guide'
page: /llm/markdown/app/releases/migration-guide.md
section: app
source_path: docs/app/releases/migration-guide.mdx
version: a0efde11117d50e2898d928874dc26c829838484
updated_at: '2026-10-23T13:29:43.731Z'
---
## Migrating to Cypress 16.0

This guide details the code changes needed to migrate to Cypress version 16.0. [See the full changelog for version v16.0](/llm/markdown/app/references/changelog.md#16-0-0).

**What you may need to change**

| Change | Action needed |
| --- | --- |
| [Native browser network in Chrome, Chromium, and Edge](#Chrome-Chromium-and-Edge-use-the-native-browser-network) | Usually none. Update tests that assert on `req.httpVersion`, compression headers, or a `304` status. |
| [Modern visibility algorithm on by default](#experimentalFastVisibility-replaced-by-visibilityStrategy) | Usually none. Update tests that relied on legacy-only visibility semantics. |
| [Browser memory management on by default](#experimentalMemoryManagement-replaced-by-manageBrowserMemory) | Only if you set `experimentalMemoryManagement: false`. Replace it with `manageBrowserMemory: false`. |
| [`keystrokeDelay` default changed from `10` to `0`](#Default-keystrokeDelay-changed-from-10-to-0) | Only if a test depended on the implicit delay. |
| [Cookie and storage reads are now queries](#cygetCookie-cygetCookies-and-cygetAllCookies-are-now-queries) | Usually none. Move assertions out of `.then()` and onto `.should()` to get retries. |
| [Node.js 20 and 25 dropped](#Nodejs-20-and-25-no-longer-supported) | Yes, if you run Cypress on either version. |
| [`Cypress.env()` removed](#Cypressenv-removed) | Yes. Migrate to `cy.env()` or `Cypress.expose()`. |
| [`cy.end()` removed](#cyend-removed) | Yes. Delete the `.end()` calls. |
| [`cy.exec()` removed](#cyexec-removed) | Yes. Replace the calls with `cy.task()`. |
| [`viewportWidth`, `viewportHeight`, and `blockHosts` via `Cypress.config()`](#viewportWidth-and-viewportHeight-can-no-longer-be-set-with-Cypressconfig-during-test-execution) | Yes, if you set them while a test is executing. |
| [`experimentalSourceRewriting` removed](#experimentalSourceRewriting-config-option-removed) | Yes. Remove the option, and enable `removeSRIAttributes` if you used it for SRI. |
| [CoffeeScript support removed](#CoffeeScript-support-removed) | Yes, if you have `.coffee` specs, support files, or fixtures. |
| [Angular, Vite, and Next.js minimums raised for component testing](#Angular-18-19-and-20-are-no-longer-supported-for-Component-Testing) | Yes, if you are below Angular 21, Vite 8, or Next.js 15.0.4. |
| [Electron deprecated as a test browser](#Electron-browser-deprecated) | Not yet, but plan to switch to an installed browser. |

### Node.js 20 and 25 no longer supported

Cypress 16 requires [Node.js](https://nodejs.org/en) **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](/llm/markdown/app/get-started/install-cypress.md#Nodejs) and [Node's release schedule](https://github.com/nodejs/Release).

### 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()`](/llm/markdown/api/commands/intercept.md) 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](/llm/markdown/app/guides/native-network-interception.md) 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](https://github.com/cypress-io/cypress/issues/new/choose) with what you are experiencing so it can be investigated.

[`forceHttp1`](/llm/markdown/app/references/configuration.md#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()`](/llm/markdown/api/commands/type.md) 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](/llm/markdown/app/references/configuration.md#Keyboard) first, then [`Cypress.Keyboard.defaults()`](/llm/markdown/api/cypress-api/keyboard-api.md), and falls back to the built-in default of `0` if neither is set. A per-command `delay` passed to [`.type()`](/llm/markdown/api/commands/type.md) 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`](/llm/markdown/app/references/configuration.md#Actionability) 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](/llm/markdown/app/core-concepts/interacting-with-elements.md#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`](/llm/markdown/app/references/configuration.md#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](/llm/markdown/app/references/configuration.md#manageBrowserMemory) for more information.

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

[`cy.getCookie()`](/llm/markdown/api/commands/getcookie.md), [`cy.getCookies()`](/llm/markdown/api/commands/getcookies.md), and [`cy.getAllCookies()`](/llm/markdown/api/commands/getallcookies.md) are now retry-able [query commands](/llm/markdown/app/core-concepts/retry-ability.md), just like [`cy.get()`](/llm/markdown/api/commands/get.md) and [`cy.contains()`](/llm/markdown/api/commands/contains.md).

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()`, and `cy.getAllCookies()`) are now governed by [`defaultCommandTimeout`](/llm/markdown/app/references/configuration.md#Timeouts) (default `4000` ms), the same timeout used by every other query.
*   **Cookie action commands** ([`cy.setCookie()`](/llm/markdown/api/commands/setcookie.md), [`cy.clearCookie()`](/llm/markdown/api/commands/clearcookie.md), [`cy.clearCookies()`](/llm/markdown/api/commands/clearcookies.md), and [`cy.clearAllCookies()`](/llm/markdown/api/commands/clearallcookies.md)) are unchanged. They make a single automation round-trip, do not retry assertions, and remain governed by [`responseTimeout`](/llm/markdown/app/references/configuration.md#Timeouts) (default `30000` ms).

The new retry behavior applies only to assertions chained directly onto the command. A [`.then()`](/llm/markdown/api/commands/then.md) 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()`](/llm/markdown/api/commands/should.md) 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()`](/llm/markdown/api/commands/then.md) 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](/llm/markdown/api/cypress-api/custom-queries.md#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](/llm/markdown/api/cypress-api/custom-queries.md#Overwriting-Existing-Queries) for more details.

### `cy.getAllLocalStorage()` and `cy.getAllSessionStorage()` are now queries

[`cy.getAllLocalStorage()`](/llm/markdown/api/commands/getalllocalstorage.md) and [`cy.getAllSessionStorage()`](/llm/markdown/api/commands/getallsessionstorage.md) are now retry-able [query commands](/llm/markdown/app/core-concepts/retry-ability.md), just like [`cy.getCookie()`](/llm/markdown/api/commands/getcookie.md) and [`cy.getCookies()`](/llm/markdown/api/commands/getcookies.md).

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`](/llm/markdown/api/commands/getalllocalstorage.md#Arguments) and accept a `timeout` option. They are governed by [`defaultCommandTimeout`](/llm/markdown/app/references/configuration.md#Timeouts) (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()`](/llm/markdown/api/commands/then.md) 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()`](/llm/markdown/api/commands/should.md) 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()`](/llm/markdown/api/commands/then.md) 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](/llm/markdown/api/cypress-api/custom-queries.md#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](/llm/markdown/api/cypress-api/custom-queries.md#Overwriting-Existing-Queries) for more details.

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

[`viewportWidth`](/llm/markdown/app/references/configuration.md#Viewport) and [`viewportHeight`](/llm/markdown/app/references/configuration.md#Viewport) can no longer be set with [`Cypress.config()`](/llm/markdown/api/cypress-api/config.md) 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()`](/llm/markdown/api/commands/viewport.md) to change the viewport during a test, or set the values in the [test configuration](/llm/markdown/app/references/configuration.md#Test-specific-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.get('#sidebar').should('not.be.visible')
    })
  }
)
```

Setting `viewportWidth` and `viewportHeight` in your Cypress configuration, or with `Cypress.config()` outside of test execution (for example at the top level of a support file or spec file), is unchanged.

### `blockHosts` can no longer be set with `Cypress.config()` during test execution

[`blockHosts`](/llm/markdown/app/references/configuration.md#blockHosts) can no longer be set with [`Cypress.config()`](/llm/markdown/api/cypress-api/config.md) while a test is executing. Doing so applied to the _next_ test rather than the current one, and the value leaked into every test that followed, so Cypress now throws instead.

Set `blockHosts` in the [test configuration](/llm/markdown/app/references/configuration.md#Test-specific-Configuration) of a `describe`, `context`, or `it` block instead, which takes effect for the suite or test where it is set and is restored afterwards.

Before

```
it('does not load analytics', () => {
  Cypress.config('blockHosts', 'www.google-analytics.com')

  cy.visit('/')
})
```

After

```
it(
  'does not load analytics',
  { blockHosts: 'www.google-analytics.com' },
  () => {
    cy.visit('/')
  }
)
```

Setting `blockHosts` in your Cypress configuration, or with `Cypress.config()` outside of test execution (for example at the top level of a support file or spec file), is unchanged.

### `experimentalSourceRewriting` config option removed

The `experimentalSourceRewriting` configuration option has been removed. The default regex-based rewriter handles every case the AST-based rewriter did and more, without the performance issues the AST-based approach could introduce.

If your `cypress.config` still sets `experimentalSourceRewriting`, Cypress will print a warning telling you to remove it. You can safely delete the option from your config.

You only need a replacement option if your application pins resources with Subresource Integrity (SRI) `integrity` attributes on `<script>` or `<link>` elements. Because Cypress rewrites those resources, the pinned hash no longer matches and the browser blocks them, reporting an error such as `Failed to find a valid digest in the 'integrity' attribute for resource '…'`. If you were relying on `experimentalSourceRewriting` to avoid this, enable the [`removeSRIAttributes`](/llm/markdown/app/references/configuration.md#removeSRIAttributes) configuration option instead, which strips the `integrity` attribute so the rewritten resources load. If your application does not use SRI, no replacement is required.

*   cypress.config.js
*   cypress.config.ts

```
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // Remove this line
  experimentalSourceRewriting: true,
})
```

```
import { defineConfig } from 'cypress'

export default defineConfig({
  // Remove this line
  experimentalSourceRewriting: true,
})
```

### `cy.end()` removed

`cy.end()` has been removed. It ended a chain of commands by yielding `null`, which was never necessary, because a Cypress chain is already terminated when the next `cy.<command>()` starts a new chain.

There is no replacement. Delete the `.end()` calls.

Before

```
cy.get('[data-cy="submit"]').click().end()

cy.get('[data-cy="name"]').type('Jane')
```

After

```
cy.get('[data-cy="submit"]').click()

cy.get('[data-cy="name"]').type('Jane')
```

`cy.end()` was deprecated in Cypress [15.15.0](/llm/markdown/app/references/changelog.md#15-15-0) and removed in 16.0.

### `cy.exec()` removed

`cy.exec()` has been removed. It ran a command through the shell on the machine running Cypress, so its behavior depended on the operating system, the shell, the login environment, and whether a terminal was attached.

Use [`cy.task()`](/llm/markdown/api/commands/task.md) instead. A task runs in the Node.js process that loads your Cypress configuration, so there is no shell resolution, no dependence on an attached terminal, and no per-platform quoting rules. It also returns real values instead of stdout you have to parse.

Register the task in `setupNodeEvents`:

*   cypress.config.js
*   cypress.config.ts

```
const { defineConfig } = require('cypress')
const fs = require('fs')
const path = require('path')

module.exports = defineConfig({
  // setupNodeEvents can be defined in either
  // the e2e or component configuration
  e2e: {
    setupNodeEvents(on, config) {
      const downloads = config.downloadsFolder

      on('task', {
        latestDownload() {
          const files = fs
            .readdirSync(downloads)
            .map((name) => ({
              name,
              time: fs.statSync(path.join(downloads, name)).mtimeMs,
            }))
            .sort((a, b) => b.time - a.time)

          return files.length ? path.join(downloads, files[0].name) : null
        },
      })
    },
  },
})
```

```
import { defineConfig } from 'cypress'
import fs from 'fs'
import path from 'path'

export default defineConfig({
  // setupNodeEvents can be defined in either
  // the e2e or component configuration
  e2e: {
    setupNodeEvents(on, config) {
      const downloads = config.downloadsFolder

      on('task', {
        latestDownload() {
          const files = fs
            .readdirSync(downloads)
            .map((name) => ({
              name,
              time: fs.statSync(path.join(downloads, name)).mtimeMs,
            }))
            .sort((a, b) => b.time - a.time)

          return files.length ? path.join(downloads, files[0].name) : null
        },
      })
    },
  },
})
```

Then call it from your test:

Before

```
cy.exec('ls -t cypress/downloads | head -1')
  .its('stdout')
  .then((filename) => {
    cy.readFile(`cypress/downloads/${filename.trim()}`)
  })
```

After

```
cy.task('latestDownload').then((filepath) => {
  cy.readFile(filepath)
})
```

If you need to run something that is not Node.js, spawn it from inside the task with `execFileSync` and an argument array, which avoids the shell entirely:

*   cypress.config.js
*   cypress.config.ts

```
const { defineConfig } = require('cypress')
const { execFileSync } = require('child_process')

module.exports = defineConfig({
  // setupNodeEvents can be defined in either
  // the e2e or component configuration
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        resetDatabase() {
          execFileSync('docker', ['exec', 'db', 'reset-script'], {
            stdio: 'pipe',
          })

          return null
        },
      })
    },
  },
})
```

```
import { defineConfig } from 'cypress'
import { execFileSync } from 'child_process'

export default defineConfig({
  // setupNodeEvents can be defined in either
  // the e2e or component configuration
  e2e: {
    setupNodeEvents(on, config) {
      on('task', {
        resetDatabase() {
          execFileSync('docker', ['exec', 'db', 'reset-script'], {
            stdio: 'pipe',
          })

          return null
        },
      })
    },
  },
})
```

A task must return a value or `null`, and tasks time out after 60 seconds by default. Use `taskTimeout` to change that.

The [`cy.task()` examples](/llm/markdown/api/commands/task.md#Examples) cover the patterns `cy.exec()` was commonly used for, including [cleaning up and verifying downloaded files](/llm/markdown/api/commands/task.md#Clean-up-and-verify-downloaded-files-with-cytask), [running an external command or CLI](/llm/markdown/api/commands/task.md#Run-an-external-command-or-CLI-with-cytask), and [seeding a database](/llm/markdown/api/commands/task.md#Seed-a-database-before-each-test-with-cytask).

The `execTimeout` configuration option has been removed alongside the command. Setting it prints a warning and is otherwise ignored, so you can delete it from your configuration.

`cy.exec()` was deprecated in Cypress [15.21.0](/llm/markdown/app/references/changelog.md#15-21-0) and removed in 16.0.

### `Cypress.env()` removed

`Cypress.env()` has been removed in Cypress 16.0. Use [`cy.env()`](/llm/markdown/api/commands/env.md) for sensitive values (API keys, tokens, secrets) or [`Cypress.expose()`](/llm/markdown/api/cypress-api/expose.md) for non-sensitive configuration that needs synchronous browser access.

See [Migrating away from Cypress.env()](#Migrating-away-from-Cypressenv) below for detailed before/after examples.

### `env` removed from test config overrides

The `env` key is no longer valid in per-test or suite config overrides. Test config overrides run in the browser, so `env` values set there could never be applied server-side. If absolutely necessary, `expose` can be used instead.

Before

```
it(
  'runs with feature flag',
  {
    env: {
      FEATURE_FLAG: true,
      API_VERSION: 'v2',
    },
  },
  () => {
    expect(Cypress.env('FEATURE_FLAG')).to.be.true
    expect(Cypress.env('API_VERSION')).eq('v2')
  }
)
```

After

```
it(
  'runs with feature flag',
  {
    expose: {
      FEATURE_FLAG: true,
      API_VERSION: 'v2',
    },
  },
  () => {
    expect(Cypress.expose('FEATURE_FLAG')).to.be.true
    expect(Cypress.expose('API_VERSION')).eq('v2')
  }
)
```

### `allowCypressEnv` config option removed

The `allowCypressEnv` configuration option has been removed. If your `cypress.config` still includes it, Cypress will print a warning telling you to remove it. There is no behavior change, so remove the key.

*   cypress.config.js
*   cypress.config.ts

```
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  // Remove this line
  allowCypressEnv: true,
})
```

```
import { defineConfig } from 'cypress'

export default defineConfig({
  // Remove this line
  allowCypressEnv: true,
})
```

### CoffeeScript support removed

Built-in CoffeeScript support has been removed. The default [`@cypress/webpack-batteries-included-preprocessor`](/llm/markdown/api/node-events/preprocessors-api.md) no longer bundles `coffee-loader` or `coffeescript`, and `.coffee` is no longer resolved for spec files, support files, or [`cy.fixture()`](/llm/markdown/api/commands/fixture.md).

Most projects are unaffected. If your project has no `.coffee` files, there is nothing to do, and dropping the CoffeeScript toolchain from the default preprocessor means one less thing to bundle on every spec startup.

If you do have `.coffee` files, the recommended path is to convert them to JavaScript or TypeScript, both of which Cypress compiles out of the box. If you need to keep CoffeeScript, add the loader to your own webpack configuration and register it with [`@cypress/webpack-preprocessor`](/llm/markdown/api/node-events/preprocessors-api.md#Examples):

cypress.config.js

```
const { defineConfig } = require('cypress')
const webpackPreprocessor = require('@cypress/webpack-preprocessor')

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      on(
        'file:preprocessor',
        webpackPreprocessor({
          webpackOptions: {
            resolve: { extensions: ['.js', '.coffee'] },
            module: {
              rules: [{ test: /\.coffee$/, use: ['coffee-loader'] }],
            },
          },
        })
      )

      return config
    },
  },
})
```

You are responsible for installing `coffee-loader` and `coffeescript` in your own project.

### `cypress info` no longer prints environment variables

`cypress info` no longer prints proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) or any `CYPRESS_*` environment variables. Those values routinely hold credentials, such as a record key, a proxy password, or an API token, and `cypress info` output is most often pasted into a bug report or a CI log, so its output is now safe to share.

The command still reports the details it is usually run for: detected browsers, application data, browser profile and binary cache paths, the Cypress version, and system platform and memory.

### Angular `18`, `19`, and `20` are no longer supported for Component Testing

Angular 18 and 19 have reached [end-of-life](https://angular.dev/reference/releases#actively-supported-versions), and Angular 20 reaches LTS end in November 2026. As a result, the minimum required Angular version for component testing is now `21.0.0`.

This allows us to ship the zoneless testing harness as the new default `cypress/angular` test harness in Cypress 16.0.

Additionally, the [`@cypress/angular-zoneless`](https://www.npmjs.com/package/@cypress/angular-zoneless) npm package is deprecated and no longer supported. It has been merged into [`@cypress/angular`](https://www.npmjs.com/package/@cypress/angular).

#### To continue using Angular below 21.0.0

If you haven't been able to migrate away from an older Angular version and still need that test harness, it can be installed independently via the [`@cypress/angular`](https://www.npmjs.com/package/@cypress/angular) `4.x.x` package from `npm`.

This test harness version is deprecated and no longer supported by Cypress. This version is intended to serve as a temporary workaround to migrate your project to Angular v21.0.0+.

```
npm install --save-dev @cypress/angular@4
```

Inside your support file (ex: `./cypress/support/component.(js|ts)`), or wherever your mount function is imported, make the following update to add `@`.

Before

```
import { mount } from 'cypress/angular'
```

After

```
import { mount } from '@cypress/angular'
```

#### If already using `cypress/angular-zoneless` and Angular 21.0.0

If you are already using `cypress/angular-zoneless`, update your imports to use `cypress/angular` instead. The zoneless harness has been merged into the `cypress/angular` test harness in Cypress 16.0.

Before

```
import { mount } from 'cypress/angular-zoneless'
```

After

```
import { mount } from 'cypress/angular'
```

### `@angular/platform-browser-dynamic` has been replaced with `@angular/platform-browser`

As of Cypress `16.0.0`, the `@angular/platform-browser-dynamic` package has been replaced with the `@angular/platform-browser` package.

This is because the `@angular/platform-browser-dynamic` package is deprecated and no longer the default.

To migrate, nothing additional is required as Angular 21 uses `@angular/platform-browser` by default. You can likely uninstall the `@angular/platform-browser-dynamic` package from your project dependencies.

### Vite `5`, `6`, and `7` are no longer supported for Component Testing

As of Cypress `16.0.0`, [`@cypress/vite-dev-server`](https://www.npmjs.com/package/@cypress/vite-dev-server) requires **Vite `8.x`** or above for component testing. Vite `5`, `6`, and `7` are no longer supported when Cypress resolves the Vite version installed in your project.

If an unsupported Vite version is detected, Cypress throws `ViteVersionNotSupportedError`:

```
ViteVersionNotSupportedError: Vite 8 is the required version to use cypress/vite-dev-server. Found Vite version v<your-version>
```

The recommended path is to upgrade your project to Vite `8`.

#### To continue using Vite `5` through `7`

If you are upgrading to Cypress `16` but still need Vite `5`, `6`, or `7` for component testing, install `7.x` of `@cypress/vite-dev-server` from `npm` and configure it explicitly as the dev server:

```
npm install --save-dev @cypress/vite-dev-server@7
```

Then configure the `devServer` in your `cypress.config.js` or `cypress.config.ts` file:

```
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/vite-dev-server'

export default defineConfig({
  component: {
    devServer(devServerConfig) {
      return devServer({
        ...devServerConfig,
        framework: '<YOUR_FRAMEWORK>',
        bundler: 'vite',
      })
    },
  },
})
```

Change `<YOUR_FRAMEWORK>` to `'react'`, `'vue'`, `'svelte'`, or another [supported framework](/llm/markdown/app/component-testing/component-framework-configuration.md) to match your component testing setup.

`@cypress/vite-dev-server@7` is deprecated and no longer supported by Cypress. It is intended as a temporary workaround until you can migrate to Vite `8`. More information on configuring a custom dev server can be found in the [Cypress Vite Dev Server documentation](https://github.com/cypress-io/cypress/blob/@cypress/vite-dev-server-v7.2.1/npm/vite-dev-server/README.md).

### Next.js `14` is no longer supported for Component Testing

As of Cypress `16.0.0`, component testing requires **Next.js `15.0.4` or newer**, or **Next.js `16`**. Next.js `14` is no longer supported.

Upgrade Next.js in your project before upgrading Cypress. See the [Next.js upgrade guide](https://nextjs.org/docs/app/guides/upgrading) for the steps that apply to your application, and [Next.js component testing](/llm/markdown/app/component-testing/react/overview.md#Nextjs) for the Cypress-side configuration.

### Electron browser deprecated

The bundled Electron browser is deprecated as a test browser in Cypress 16.0.0 and will be removed in a future major version. If your runs rely on the implicit Electron default, set a [`defaultBrowser`](/llm/markdown/app/references/configuration.md#Browser) (for example, `'chrome'`) and/or pass [`--browser`](/llm/markdown/app/references/command-line.md#cypress-run) to switch to an installed browser. See [Migrating away from the Electron browser](#Migrating-away-from-the-Electron-browser) for the full guide.
