View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
/Tools Library/Cypress
Tools Library · We run this

Cypress review: what it is, tutorial and pricing

Cypress is a JavaScript end-to-end and component testing framework that runs test code inside the browser, next to the application, rather than driving it from outside. It bundles the runner, assertions, network stubbing and an interactive debugger that time-travels through every command. The app is free under the MIT licence; Cypress Cloud, a paid hosted service, adds recording, orchestration and analytics.

QAble runs this tool on client engagements. The assessment below is from that experience.

last reviewed
13 October 2026
version covered
Cypress 16
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Cypress at a glance

Official site
Vendor
Cypress.io
Category
Web automation, Unit testing
Licence
Open source (MIT, with paid Cypress Cloud)
Platforms
Chrome, Chromium, Edge, Firefox, WebKit (experimental), Electron (deprecated)
Languages
JavaScript, TypeScript
Integrates with
Cypress Cloud, GitHub Actions, Jenkins, Percy
First released
2017
Version reviewed
Cypress 16

Verdict

Our verdict on Cypress

Strengths

  • The command log restores a DOM snapshot for every step, so most failures are diagnosed without re-running
  • Queries and assertions retry automatically until they pass or time out, which removes most hand-written waits
  • cy.intercept stubs, delays or fails any request the app makes, so error and empty states take minutes to test
  • Component testing mounts React, Vue, Angular and Svelte components in a real browser with the end-to-end API
  • Free MIT-licensed app from one npm install: no drivers, grid or separate assertion library to assemble

Weaknesses

  • Cannot drive two browsers or several tabs natively; multi-tab needs a Chromium-only plugin in beta
  • No real Safari: WebKit support is experimental, and Test Replay excludes Firefox and WebKit runs
  • The command queue looks synchronous but is not, which trips developers who expect async and await
  • Spec-level load balancing across machines needs Cypress Cloud, whose free plan caps results at 500 a month

Best for

JavaScript and TypeScript teams testing a web application they own, where developers write and debug tests alongside the code. The interactive runner and command log make a failing step quicker to understand than in any other web tool we run, and component testing in a real browser lets a front-end team use one tool and one set of habits for both layers. It is strongest on single-user journeys in Chrome, Edge and Firefox, which describes most product test suites.

Not for

Journeys that need two users or two browsers at once, such as chat or collaborative editing, genuine Safari coverage, native mobile, or a test team that writes Java or Python. Multi-tab work exists only through a Chromium-only plugin that is still in public beta, and WebKit support is experimental. Teams that want spec-level load balancing across CI machines without a vendor account should also look elsewhere, because that balancing is done by Cypress Cloud.

Setup cost

Low. One npm install brings the runner and its binary, and cypress open scaffolds the config, support files and example specs. A useful first test exists within the hour. The real setup work is elsewhere: a programmatic login wrapped in cy.session, a data-cy attribute convention agreed with developers, test data seeded through cy.task, and an early decision on whether CI runs through Cypress Cloud, a community spec splitter, or a single machine.

Maintenance burden

Moderate. Retry-ability removes most waiting code, but the command queue is not ordinary async JavaScript, and suites written by people who treat it as such fail in ways that look random. Major versions also remove things deliberately: Cypress 16 removed Cypress.env() and cy.exec(), deprecated Electron as a test browser and raised the Node.js floor to 22. Budget a proper upgrade pass for each major rather than bumping the version in passing.

Honest limitations

One browser, one tab and one superdomain per test unless you reach for cy.origin, all of which follow from running inside the browser. No real Safari, only experimental WebKit. No native events, so hover and some keyboard behaviour are simulated rather than real. JavaScript and TypeScript only. Test Replay records Chromium-family runs only. The free Cloud plan now includes parallelisation, but its 500 recorded results a month are used up quickly by any real suite.

How it works

What is Cypress and how does it work?

The test runs inside the browser

Cypress launches a browser, loads your application in a frame inside it, and runs your spec in the same browser. Because the test shares the browser with the application, it can read the DOM directly, reach the window object and see changes as they happen, rather than asking a driver over a protocol. A Node.js process runs alongside, handling the file system, screenshots, tasks and network control. That split explains most of Cypress’s character: commands that touch the page are fast and observable, anything that needs the operating system goes through cy.task, and anything outside one tab, such as a second window or a second browser, is out of reach by design.

Commands are queued, not executed

When a test function runs, each cy call is added to a queue and returns immediately; Cypress then works through the queue one command at a time. That is why you cannot assign the result of cy.get to a variable and use it on the next line, and why mixing cy commands with plain async code produces values that seem to arrive in the wrong order. Results come out through .then, through aliases created with .as, or through assertions. Once the model is understood the code reads cleanly, with no await on every line. Teams that skip it write tests that pass alone and fail when run in sequence.

Retry-ability replaces explicit waits

Queries such as cy.get, .find and .contains link together, and when an assertion after them fails Cypress re-runs the whole chain of queries from the top, for up to four seconds by default (defaultCommandTimeout). Actions such as .click and .type run once, but first wait until the element is visible, enabled and not covered. The effect is that most synchronisation is implicit: you assert what should be true and Cypress waits for it. The catch is that a .should placed mid-chain locks in its subject once it passes, so if the framework re-renders after that point, later commands act on a detached element.

Network control on the browser’s own stack

Cypress has always sat between the browser and the network so cy.intercept can see and rewrite traffic. From Cypress 16, Chrome, Chromium and Edge intercept on the browser’s native network path, which brings HTTP/2 and HTTP/3 support; Firefox, WebKit and Electron stay on the older proxy path. For most suites the change is invisible, but on Chromium intercepted requests no longer report an HTTP version and intercepted responses lack compression headers. A forceHttp1 option, deprecated from the day it was added, routes every browser through the old path while you adjust.

Cloud orchestrates, your machines execute

Cypress Cloud runs no tests. Your CI machines run them and report to Cloud when started with --record. Cloud then provides what a recorded run makes possible: load balancing across machines, Test Replay of CI failures, flake detection and analytics. Parallelisation depends on it because the balancing is central: each machine asks Cloud for the next spec, and Cloud hands them out using each spec’s previous durations to keep total run time down. Without Cloud, each machine has to be given its share of specs up front by a splitting script.

Strengths

What it does well

An interactive runner built for writing tests

cypress open shows the application beside a command log. Hovering a command restores the DOM as it was at that moment, and clicking it prints the subject and the yielded value to the browser console. For someone writing a new test this tightens the loop more than any other feature, because you see exactly what Cypress saw at each step instead of reconstructing it from a stack trace.

Stubbing and spying on network traffic

cy.intercept matches requests by method, path, query or headers, and can spy on them, stub them with a fixture, add a delay, rewrite the response or force a network error. Waiting on an aliased request is the reliable way to synchronise with the backend, and stubbing makes empty, error and slow states testable without changing the server.

Component tests with the same tools

cy.mount renders one component in a real browser, with the same commands, assertions and debugger as end-to-end tests. Cypress 16 supports React 18 and later, Next.js 15 and later, Vue 3 and Angular 21 and later, with Svelte 5 in alpha, through Vite 8 or Webpack 5. Testing a date picker or a form in isolation is faster and more focused than reaching it through the whole application.

Cached sessions with cy.session

cy.session runs a login once, caches the cookies and storage it produced, and restores them in later tests, optionally across spec files in the same run. Combined with a login through cy.request rather than the form, it removes the slowest repeated work in most suites while keeping each test isolated, since test isolation still clears the page and storage between tests.

Test Replay for CI failures

For runs recorded to Cypress Cloud in Chrome, Edge or Electron, Test Replay captures commands, DOM changes, network requests, console output and errors, so a CI failure can be stepped through much like a local run. It is included on every Cloud plan, the free one included, within that plan’s result limit. Firefox and WebKit runs are not captured.

Natural-language steps with cy.prompt

cy.prompt, introduced in Cypress 15.4 and now in beta, turns plain-English steps into Cypress commands and lets you save the generated code. It needs a Cloud account or record key, runs in Chrome and Edge for end-to-end tests only, and every execution counts against a plan allowance. It is useful for drafting a test quickly; review and save the generated commands before relying on them in CI.

Pricing

Cypress pricing and licensing

The Cypress app is free and open source under the MIT licence, with nothing held back in the runner itself. Money enters through Cypress Cloud, a hosted service for recorded runs, priced by users and recorded test results, where each it() block that runs counts as one result. There is a free Starter plan, Team and Business plans billed annually, and Enterprise on quote. CI machines are always yours to pay for, whichever plan you choose.

  • Licence: MIT for the app, stated in the repository and on the pricing page. Any number of tests on any number of machines costs nothing unless you record to Cloud.
  • Starter (free): 10 users, 500 recorded test results a month and 30 days of retention, with parallelisation, load balancing and Test Replay included. There is no overage option, so it suits trials and small projects.
  • Team and Business: annual plans with 50 users, 120,000 results a year and 90 days of retention. Team adds flake detection, Jira issue creation and webhooks; Business adds spec prioritisation, auto cancellation and SSO. List prices on cypress.io on 2026-10-13 started at $799 a year for Team and $3,199 a year for Business.
  • Enterprise: custom quote, unlimited users, 1.8 million results a year and 180 days of retention. UI Coverage and Accessibility are separately priced add-ons on the paid plans.
  • How cost scales: by recorded results, not by machines. A 300-test suite run on every pull request passes the free monthly allowance on its second run, so estimate results per month before choosing; extra results on Team and Business are billed on demand.
  • Open source projects: Cloud is free for non-commercial public projects, and the pricing page describes an OSS plan with a larger result allowance for eligible projects.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Cypress tutorial: getting started

Setup, step by step

  1. Check Node.js: Cypress 16 needs 22.x, 24.x or 26 and later.
  2. Install Cypress as a dev dependency in the repository that holds your application.
  3. Run npx cypress open, choose E2E Testing, and let it write cypress.config.js and the support files.
  4. Set e2e.baseUrl in cypress.config.js to the address of your locally running application.
  5. Create a spec under cypress/e2e with the .cy.js suffix and run it in Chrome from the launchpad.
  6. Run npx cypress run --browser chrome to confirm it passes headless, the way CI will run it.

Install

npm install cypress --save-dev
# then open the app and let it scaffold the project
npx cypress open

A first example

// cypress/e2e/login.cy.js
// Assumes cypress.config.js sets e2e: { baseUrl: 'http://localhost:3000' }
describe('sign in', () => {
  it('takes a valid user to the dashboard', () => {
    cy.visit('/login')

    // data-cy attributes survive styling and markup changes
    cy.get('[data-cy=email]').type('[email protected]')
    cy.get('[data-cy=password]').type('correct-horse', { log: false })
    cy.get('[data-cy=submit]').click()

    // Assertions retry for up to 4 seconds, so no wait is needed
    cy.location('pathname').should('eq', '/dashboard')
    cy.contains('h1', 'Welcome back').should('be.visible')
  })
})

What trips people up first

Treating commands as values. The first hour usually includes someone assigning the result of cy.get to a variable, or putting await in front of a command, and getting something that is not an element. Commands are queued and run later, so they return a chainable rather than a value: use .then to work with what a command yields, .as to name it for later, and .should to wait for a state. The second hurdle is the browser. cypress run still defaults to the bundled Electron browser, now deprecated, so pass --browser chrome or set defaultBrowser so CI runs what you tested in the launchpad.

In practice

Using Cypress in practice

Project layout and selector discipline

Keep the default layout and organise by feature rather than by page: cypress/e2e/checkout, cypress/e2e/account, with fixtures in cypress/fixtures and shared commands in cypress/support. Component specs usually sit next to the components they test. Agree a data-cy attribute convention with developers early, because selectors tied to classes or DOM structure are the main source of breakage after a redesign. Cypress’s own best-practice guide argues against classic page objects in favour of custom commands and programmatic state, and in our experience that is right for Cypress specifically: the command queue makes page object classes awkward, while a small set of named commands such as cy.login or cy.createOrder reads well and keeps setup out of the user interface. Keep tests independent; test isolation is on by default and clears the page, cookies and storage before each test.

Logging in once with cy.session

Logging in through the form in every test is the largest avoidable cost in most suites. Wrap a programmatic login in cy.session so it runs once per user and is restored afterwards, and add a validate function so a stale or expired session is rebuilt rather than producing a confusing failure. Secrets come from cy.env in Cypress 16, which reads values set under env in the config, from cypress.env.json or from CYPRESS_ prefixed environment variables in CI.

// cypress/support/commands.js
Cypress.Commands.add('login', (email) => {
  cy.session(
    email,
    () => {
      // cy.env replaces Cypress.env(), which Cypress 16 removed
      cy.env(['password']).then(({ password }) => {
        // Assumes the API sets a session cookie on success
        cy.request('POST', '/api/login', { email, password })
          .its('status')
          .should('eq', 200)
      })
    },
    {
      // Runs after a cached session is restored; failure re-runs setup
      validate() {
        cy.request('/api/me').its('status').should('eq', 200)
      },
      cacheAcrossSpecs: true,
    }
  )
})

// cypress/e2e/orders.cy.js
beforeEach(() => {
  cy.login('[email protected]')
  cy.visit('/orders')
})

Testing edge states by stubbing the API

Most user-facing bugs live in the states that are hard to produce against a real backend: empty lists, server errors and slow responses. cy.intercept makes each of those a few lines. Match on method and pathname rather than a full URL so the route survives environment changes, alias it, and wait on the alias before asserting, which synchronises with the request rather than with a guessed delay. Keep a smaller set of unstubbed journeys that exercise the real API, so stubs cannot drift from the contract unnoticed.

describe('orders page', () => {
  beforeEach(() => cy.login('[email protected]'))

  it('lists orders from the API', () => {
    // Serves cypress/fixtures/orders.json instead of the real response
    cy.intercept({ method: 'GET', pathname: '/api/orders' }, { fixture: 'orders.json' }).as('orders')
    cy.visit('/orders')
    cy.wait('@orders')
    cy.get('[data-cy=order-row]').should('have.length', 3)
  })

  it('offers a retry when the API fails', () => {
    cy.intercept({ method: 'GET', pathname: '/api/orders' }, { statusCode: 500, body: {} }).as('orders')
    cy.visit('/orders')
    cy.wait('@orders').its('response.statusCode').should('eq', 500)
    cy.contains('[data-cy=error]', 'Try again').should('be.visible')
  })
})

Running in GitHub Actions

The official cypress-io/github-action (v7) installs dependencies, caches the Cypress binary, builds and starts the application, waits for it and runs the tests. With Cypress Cloud, a matrix of identical jobs plus record and parallel turns them into one balanced run. Use a cypress/browsers container so every machine has the same browser version, and keep fail-fast off so one failure does not kill machines that Cloud is still handing specs to. Without Cloud, drop record and parallel and run a single job, or use a community plugin such as cypress-split to give each matrix job a fixed share of specs. Screenshots of failures are saved by default; video is off by default since Cypress 13.

# .github/workflows/e2e.yml
name: e2e
on: [push, pull_request]
jobs:
  cypress:
    runs-on: ubuntu-latest
    strategy:
      # Keep siblings running: Cloud is still handing them specs
      fail-fast: false
      matrix:
        machine: [1, 2, 3]
    container:
      # Same browser version on every machine
      image: cypress/browsers:latest
      options: --user 1001
    steps:
      - uses: actions/checkout@v7
      - uses: cypress-io/github-action@v7
        with:
          build: npm run build
          start: npm start
          wait-on: 'http://localhost:3000'
          browser: chrome
          record: true
          parallel: true
          group: 'e2e-chrome'
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/upload-artifact@v7
        if: failure()
        with:
          name: screenshots-${{ matrix.machine }}
          path: cypress/screenshots
          if-no-files-found: ignore

Upgrading to Cypress 16 and keeping the config honest

Cypress 16 is a breaking release, and most of the breakage lands in configuration and support code rather than in specs. Cypress.env() is gone: public values move to an expose block read with Cypress.expose, and secrets stay under env and are read with cy.env, which keeps them in the Node process instead of the browser. cy.exec is gone: move shell work into a named task. Set a default browser so nothing depends on Electron, and turn on retries in run mode only, so flaky tests are retried in CI but stay visible when you develop. Component testing now needs Vite 8 or later, Angular 21 or later, or Next.js 15 or later.

// cypress.config.js
const { defineConfig } = require('cypress')
const { execSync } = require('node:child_process')

module.exports = defineConfig({
  defaultBrowser: 'chrome',
  // Public values: readable in the browser with Cypress.expose('apiUrl')
  expose: { apiUrl: 'http://localhost:3000/api' },
  // Secrets: read in tests with cy.env(['adminToken'])
  env: { adminToken: process.env.ADMIN_TOKEN },
  e2e: {
    baseUrl: 'http://localhost:3000',
    retries: { runMode: 2, openMode: 0 },
    setupNodeEvents(on) {
      on('task', {
        // Replaces cy.exec('npm run db:reset'); call with cy.task('resetDb')
        resetDb() {
          execSync('npm run db:reset', { stdio: 'inherit' })
          return null // a task must not return undefined
        },
      })
    },
  },
})

Pitfalls

Common pitfalls

Assigning command results to variables

Why it happens: Commands are enqueued and run after the test function returns, so a variable assigned from cy.get holds a chainable, not an element or text. Code that reads it on the next line sees nothing useful, and adding await makes it worse.

The fix: Work inside .then for values you need in plain JavaScript, use .as to alias a value and read it later with cy.get on the alias or this in a function callback, and prefer assertions over extracting values at all.

Fixed waits with cy.wait and a number

Why it happens: It looks like the fix for a race, and it hides the race on a fast machine. On a loaded CI runner the delay is too short, and everywhere else it is dead time added to every run.

The fix: Wait on what you depend on: an aliased cy.intercept request, or an assertion on the element or URL that shows the state has arrived. Cypress’s own best-practice guide lists numeric waits as an anti-pattern.

Detached DOM errors in React and Vue applications

Why it happens: A query chain was locked in by an assertion or a .then, and the framework then re-rendered and replaced the element. The next action targets a node no longer in the document.

The fix: Keep queries chained straight into the action, put assertions after the action or on a fresh query, and assert on the state that shows rendering has finished, such as a loaded list length, before interacting.

Following tutorials written for older versions

Why it happens: Years of articles use Cypress.env(), cy.exec, the old plugins file and cypress.json. On Cypress 16 the first two throw, and the others were replaced years ago, so copied code fails in confusing ways.

The fix: Read the migration guide for every major between the tutorial and your version. Use Cypress.expose or cy.env for configuration, cy.task for shell or database work, and cypress.config.js with setupNodeEvents.

Tests that depend on the previous test

Why it happens: Test isolation is on by default, so the page, cookies and storage are cleared before each test. A test that relied on the previous one to log in or create data fails when run alone or in a different order.

The fix: Give each test its own setup in beforeEach: cy.session for login, cy.request or cy.task for data. Every test should pass when run on its own with .only.

Re-running failed jobs on a parallel recorded run

Why it happens: GitHub’s re-run failed jobs option looks like it re-runs the failed tests. With Cypress Cloud parallelisation it re-runs the whole suite, load-balanced over only the machines whose jobs failed.

The fix: Re-run the whole workflow, or fix and push. If reruns are frequent, address the flaky specs Cloud’s flake detection identifies rather than relying on reruns.

Compared

Cypress alternatives compared

ToolLanguagesWhere tests runMulti-tab and multi-userSafariParallel across machinesComponent testing
CypressJavaScript, TypeScriptInside the browserNo; beta plugin for Chromium tabsExperimental WebKit onlyVia Cypress Cloud, free tier cappedYes, in a real browser
PlaywrightJS, TS, Python, Java, C#Outside, over a WebSocketYes, contexts and pagesWebKit engine, not SafariBuilt-in sharding, freeYes, mount fixture
SeleniumJava, Python, C#, Ruby, JSOutside, over WebDriverYesYes, real SafariVia Grid plus frameworkNo
WebdriverIOJavaScript, TypeScriptOutside, WebDriver BiDi or ClassicYes, plus multiremoteYes, real SafariBuilt-in workers and sharding, freeYes, browser runner

FAQ

Frequently asked questions

What is Cypress?

Cypress is a JavaScript end-to-end and component testing framework that runs test code inside the browser, next to the application, rather than driving it from outside. It bundles the runner, assertions, network stubbing and an interactive debugger that time-travels through every command. The app is free under the MIT licence; Cypress Cloud, a paid hosted service, adds recording, orchestration and analytics.

What are the best Cypress alternatives?

The alternatives most often weighed against Cypress are Playwright, Selenium and WebdriverIO. The comparison on this page sets them side by side on languages, where tests run, multi-tab and multi-user, safari, parallel across machines and component testing, and the verdict above says where Cypress is the wrong choice and which of them to pick instead.

Is Cypress free?

The Cypress app is free and open source under the MIT licence, and you can run any number of tests on any number of machines without paying. Cypress Cloud, the hosted service for recording runs, load balancing, Test Replay and analytics, has a free Starter plan with 500 recorded test results a month, then paid Team, Business and Enterprise plans priced mainly by recorded results.

Should I choose Cypress or Playwright?

Choose Cypress when a JavaScript team values the interactive runner and debugger, tests one user in one tab, and wants component and end-to-end tests in one tool. Choose Playwright when you need several tabs or users in one test, WebKit coverage, languages beyond JavaScript, or free parallel execution across machines without a hosted service. For a new suite with no strong preference, Playwright has fewer structural limits.

Can Cypress test Safari?

Not real Safari. Cypress supports Chrome, Chromium, Edge and Firefox, plus experimental WebKit, the engine Safari is built on, enabled with experimentalWebKitSupport and the playwright-webkit package. WebKit catches many engine differences but not Apple-specific behaviour, so Safari-critical journeys still need checking on a real device or a cloud Safari session.

Can Cypress handle multiple tabs or two users at once?

Not natively. Cypress cannot control more than one browser at a time, and a test stays in one tab. The @cypress/puppeteer plugin, in public beta, can open and drive extra tabs in Chromium-family browsers. A common workaround is to remove the target attribute from a link so it opens in the same tab, or to act as the second user through the API. Genuine two-user flows are a better fit for Playwright or WebdriverIO multiremote.

How do I run Cypress tests in parallel without Cypress Cloud?

The built-in parallel flag needs a recorded run, which means Cloud. Without it, split spec files across CI jobs yourself: a matrix in GitHub Actions where each job runs a subset with --spec, or a community plugin such as cypress-split that divides specs between jobs automatically. You lose central load balancing and Test Replay, so keep spec files of similar length to avoid one job finishing long after the others.

Why does Cypress.env() no longer work?

Cypress 16 removed it. Values that are safe to be readable in the browser now go in an expose block and are read synchronously with Cypress.expose. Secrets stay under env, in cypress.env.json or in CYPRESS_ environment variables, and are read inside a test with cy.env, which yields only the keys you ask for and keeps secrets out of the browser.

Is Cypress still worth learning in 2026?

Yes, if you work on JavaScript web applications. It has a large installed base, active releases, a strong component testing story and the best interactive debugger in the category, so existing suites will need maintaining for years. If you are choosing a first browser tool with no existing codebase to support, learn Playwright as well, since its model transfers more broadly.

Alternatives

Cypress alternatives worth weighing

Resources

Where to go next

Keep going

Prepare, or get it done

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor