QAAll levels

Cypress

Cypress is an open-source test runner for web applications that runs the test inside the browser, in the same tab and event loop as the application. The page opens with the twelve journeys from End-to-end testing as the shop would write them in Cypress — seven native, five that need a workaround, and the one thing all five have in common — then covers the tab and the four trade-offs Cypress calls permanent, the command queue, retry-ability as it has worked since Cypress 12, isolation and the Node side, parallelism by spec file, four changes underneath working suites, and where it does not help.

Current version
16.2 · October 2026
Test runs
Inside the browser
Parallel by
Spec file, via the Cloud

The shop’s twelve journeys in Cypress on one screen — native, or a workaround

The question a team brings to this page is whether Cypress fits its product, and the honest way to answer it is journey by journey. Below are the twelve journeys from the End-to-end testing page — the same online shop — as the shop would write them in Cypress. The third column says what Cypress needs to do each one; the fourth gives a verdict. Native means the ordinary does it. A workaround means it can be done, with a setting, a separate block or an experimental flag, and with a cost the row names. Later sections cite the journeys by number.

#JourneyIn CypressVerdict
1Search “blue jumper”, open SKU 30417, pay with the test cardThe card fields are the payment provider’s iframe on another origin. Cypress does not automate a cross-origin iframe unless web security is switched off with chromeWebSecurity: false — in Chrome-family browsers onlyWorkaround · Chrome-family only
2Sign up, confirm, sign in, reload — in Chromium and WebKitWebKit runs only behind experimentalWebKitSupport, with Playwright’s WebKit build installed as playwright-webkitWorkaround · experimental
3Reset a forgotten password from the emailed linkcy.task() reads the test inbox in Node; cy.visit() opens the linkNative
4Apply SPRING10, change the quantity, payThe same card iframe as journey 1; cy.request() reads the charge from the sandboxWorkaround · Chrome-family only
5Pay with 3-D Secure: out to the bank’s check and backThe card iframe, and then the bank’s page on another origin: its steps go inside a cy.origin() block, which receives values only through argsWorkaround · two of them
6A card is declined; pay againThe card iframe; cy.request() counts the orders afterwardsWorkaround · Chrome-family only
7Ask the order assistant “where is my order”The chat widget is the shop’s own page. The model’s replay is set on the server, as with Playwright — cy.intercept() sees only what the browser sendsNative
8Start a return on day 30; the label arrives by emailcy.clock() sets the browser’s date; the server’s test clock is set through cy.request()Native
9The warehouse confirms the return; the refund landscy.request() posts the warehouse’s webhook from Node, where CORS does not apply; cy.task() polls the inboxNative
10Change the delivery address before dispatchcy.request() reads the warehouse’s test endpointNative
11Check out as a guest, then create an accountTest isolation clears the page, cookies and both web storages before each test — though not IndexedDBNative
12A support agent refunds a damaged item in the back officecy.session() signs the agent in once and restores that session for every test that asks for itNative

A team can avoid the by paying through the provider’s API with a test token instead of typing a card. The journey then runs everywhere, and it stops proving the seam it exists for — row 4 on the End-to-end testing page, that the total on the screen is the amount the provider is asked to charge. That is the shape of most Cypress decisions. The tool offers a way around each edge, and each way around moves the test a little further from what a customer does.

The test lives in the tab — and the four trade-offs Cypress calls permanent

Most browser automation is remote control: the test is a program somewhere else that sends instructions to a browser and reads answers back. Cypress inverts it. Your application is loaded into an iframe, the test is loaded beside it in the same tab, and both run on the same JavaScript . There is no wire between the test and the page, so the test can reach into the application the way a script on the page could. Anything outside the browser — a file, a database, the test inbox — goes through a second, Node.js process that the test talks to with cy.task().

Cypress does something no other browser tool does: it loads your application into an iframe and runs the test alongside it, in the same tab and the same event loop. That is why the debugging experience is what it is. The runner can reach into the application directly rather than asking a driver to relay a question, so it snapshots the DOM at every command and lets you scrub back through the run afterwards, and it re-runs an assertion until the page settles instead of failing the first time it looks. The same decision draws the ceiling. Anything outside the browser — reading a file, seeding a database, stubbing a network call at the process level — needs a second process that the test talks to, and anything outside the tab, such as a second browser window, an OS file dialog or a cross-origin redirect chain, is awkward at best. Neither half of that is a defect. It is one architectural choice with both of its consequences visible.

Since Cypress 16, released on 1 September 2026, one part of that picture moved. In Chrome, Chromium and Edge, Cypress now intercepts test traffic on the browser’s own network instead of routing it through its proxy, so the application is tested over /2 and HTTP/3 with its real certificate. Firefox, WebKit and Electron still take the old path through the proxy. The same cy.intercept() therefore runs over two different network paths depending on the browser, and a few of its behaviours differ between them.

Cypress’s own documentation lists the costs of living in the tab under the heading “Trade-offs”, and it calls four of them permanent. They are worth reading in its words, because they are not configuration problems a later version will remove.

Permanent trade-offWhat it meansIn the shop
“Cypress is not a general purpose automation tool”Its “sweet spot” is testing your own application — not indexing or spidering the web, performance testing or scripting third-party sitesFits: every journey tests the shop
“Cypress commands run inside of a browser”Test code is JavaScript evaluated in the browser; the back end is reached through cy.task() and cy.request()The inbox and the warehouse go through Node (3, 9, 10)
“You cannot use Cypress to drive two browsers at the same time”One browser at a time; the documentation suggests stubbing the other side, and a plugin, @cypress/puppeteer, for several tabsJourney 12 is sequential, so it fits; a live chat between a customer and an agent would not
“Each test is bound to a single superdomain”Crossing to another origin needs a cy.origin() block, and since Cypress 14 a subdomain counts as another originThe bank’s 3-D Secure page (5)

The same page lists three “temporary” ones that Cypress hopes to address: there is no cy.hover(), there is no support for native or mobile events, and iframe support “is somewhat limited” — same-origin frames can be queried, cross-origin ones cannot. The last of those is the one that decided four of the shop’s verdicts.

Commands are queued, not run — why `await`, `try/catch` and `if` misbehave

The consequence people meet first is that Cypress commands are not what they look like. The documentation puts it plainly: commands “don’t do anything at the moment they are invoked, but rather enqueue themselves to be run later.” A line asking for an element does not fetch it and does not return it. It appends a command to a queue that starts running only after the whole test function has finished. So a value cannot be assigned from a command into a variable and used on the next line: when the next line is read, nothing has run yet.

Reading a Cypress test top to bottom takes about a millisecond, and nothing on the page happens during it. Each cy line only adds a command to a queue: visiting the shop, typing into the search box, clicking the product and checking the price are all queued, not done. Line 4 assigns what cy.get returns to a constant, and what it returns is a chainer — an object for adding more commands — not the price on the page. Line 5's if statement runs immediately, while the test function is still being read, and compares that chainer with a string, so it is false and never will be anything else. Only when the function returns does the queue start, running lines 1, 2, 3 and 6 in order over about three seconds; by then lines 4 and 5 are long over. That is why await does nothing useful, why a try and catch around a command catches nothing, and why a value comes back through .then() or an alias and a check through .should().

The queue also explains the debugging experience people choose Cypress for. Because the runner executes each command itself, inside the page, it keeps a snapshot of the page for every command, and the Command Log lets you hover over a step and see the page as it was at that moment. Commands run one after another, never in parallel, and the documentation calls that an intentional decision rather than a technical limit: the point is a test that behaves identically from one run to the next.

Retry-ability since Cypress 12: a chain of queries retries from the top

Cypress does not wait a fixed time for the page. It splits commands into kinds and retries the ones that only read. Queries — cy.get(), .find(), .contains(), .eq() and the like — link up with the assertions after them, and if an assertion fails, Cypress runs the whole chain of queries again from the top, for up to four seconds by default. Actions such as .click() and .type() are not queries: they wait until the element is actionable, re-running the queries before them while they wait, and then act once.

Advice written before December 2022, and much that still copies it, says the opposite: that only the last query before an assertion retries, so a list that re-renders leaves the test holding a detached element. That was Cypress up to version 11. Cypress 12 changed it, and the rule that replaced it has one boundary worth learning. An assertion in the middle of a chain locks the subject once it passes; queries after it retry from that locked subject, not from the top. If the page re-renders between the two assertions, the locked element is gone and the test fails.

Since Cypress 12, queries link up with the assertion after them and retry together. In the top row, cy.get finds the basket's items, first takes the first one, and the assertion checks that it contains SKU 30417; while the assertion fails, Cypress runs the whole chain again from cy.get, for up to four seconds, so a list that re-renders is simply found again. Once the assertion passes, the subject is locked in. The second row continues from that locked li: find the quantity, check that it reads 2. If that assertion fails, the retry re-runs only from find, against the li already locked — and if the basket re-rendered in between, that li is detached from the page and the test fails, however correct the page now is. The documented fixes are to split the chain at the first assertion so the second half starts again from cy.get, or to put both checks into one should with a callback, so they retry together. Before Cypress 12, only the last query in a chain retried at all, which is the behaviour much older advice still describes.
What you wroteWhat retriesWhat to do
Queries, then an assertionThe whole chain of queries, from the topNothing — this is the shape the model is built for
Queries, an assertion, more queries, an assertionUp to the first assertion; after it, only from the locked subjectSplit the chain at the assertion, or put both checks in one .should(callback)
An action — .click(), .type()Waits for actionability, re-running the queries before it; then acts onceAssert the state the action produced, not the action
A value taken in .then() and checked laterNothing: the value is frozen when it is readAssert on the live element instead
cy.getCookie() and storage readsSince Cypress 16, they are queries and retry tooChain the assertion directly to them

Isolation, sessions and the Node side

Since Cypress 12, test isolation is on by default. Before each test Cypress visits about:blank and clears , localStorage and sessionStorage in all domains, so one journey does not inherit another’s state. The difference from Playwright is worth knowing exactly. Playwright gives each test a new , a whole new profile. Cypress cleans the same browser between tests, and its documentation says what it leaves: “IndexedDB and other browser storage mechanisms are not cleared.” A shop that kept a guest basket in IndexedDB would have to clear it itself before journey 11.

NeedCommandRuns inWorth knowing
Sign in once, reuse itcy.session()The browserCaches and restores cookies and both web storages under an id; validate checks a restored session still works (journey 12)
Call an APIcy.request()Node, on Cypress’s sideNot bound by CORS or the same-origin policy, which is why it can post the warehouse’s webhook (journey 9)
Anything else outside the browsercy.task()Node, in your config’s setupNodeEventsReading the inbox, seeding the database. cy.exec(), which ran shell commands, was removed in Cypress 16 in its favour
A configuration valueCypress.expose() or cy.env()The browser, or NodeCypress 16 removed Cypress.env() and made the choice explicit: a value exposed to the browser is readable by the application under test and everything it loads, so a secret stays in Node

Reaching into the application deserves a warning of its own. Because the test shares the page, it can call the application’s functions, read its store or spy on a method. That is excellent for arranging state that would take six clicks to reach, and poor for asserting behaviour: a check written against internals is coupled to how the feature is built, so a refactor that changes nothing a customer sees breaks the suite. Arrange through the inside; assert through what a person sees.

Parallel by spec file, orchestrated by Cypress Cloud

Cypress runs one browser at a time per machine, so a faster suite means more machines. Its parallelization is file-based: whole spec files are handed to machines, never single tests. The handing out is done by Cypress Cloud. Each machine runs cypress run --record --parallel, Cloud estimates every spec’s duration from earlier runs, and it gives each machine one spec at a time until none are left. Without Cloud a team can still split specs across machines by hand, but the split is static and does not learn.

Cypress parallelises by spec file: whole files go to machines, never single tests. The same twelve journeys, 790 seconds in Chromium, are drawn twice on four machines. Grouped into four files by feature, checkout.cy holds journeys 1, 4, 5 and 6 — 315 seconds — and the run cannot finish before that file does, however many machines there are, while the other three machines sit idle from 150 or 175 seconds. Given one file each, the journeys can be handed out longest first, which is what Cypress Cloud does with its estimates of each spec's duration, and the run ends at 205 seconds. Splitting the twelve files across the machines by hand, three in a row each, ends at 225. With twelve journeys the difference is about two minutes; with hundreds of specs of uneven length it decides whether the suite fits a merge. Balancing by duration needs the --record flag and Cypress Cloud; without them, the split is made by hand and does not learn.

Cloud is also where a CI failure is debugged. Test Replay, added in Cypress 13 in August 2023, records the DOM, network requests and console of each test in CI and lets a developer step through the run in the browser afterwards; the documentation says it is available on all Cloud plans at no extra cost, subject to usage limits. Video recording has been off by default since the same release. Both facts point one way: the parts of the Cypress experience that matter most in CI — balancing and replay — are a hosted service, which is a choice to make with open eyes, not a detail to discover.

Four changes underneath working suites

Each of these is a dated change published by Cypress itself. None is a defect. Each moved something a suite had come to rely on, and the third column is what made the move an afternoon instead of a week.

ChangeWhat it exposedWhat protected a suite
Cypress 12, 6 December 2022: test isolation on by default; queries retry as a chainTests that relied on the page or the cookies an earlier test left behind failed after the upgrade, though nothing in the product had changed.Tests that were independent from the first day. testIsolation: false exists as a migration path — a crutch worth putting a removal date on.
Cypress 13, 2023: a third-party module that sent runs to Sorry-Cypress and Currents is blocked; on 7 November the block extends to v12Projects whose parallel runs were orchestrated by those services could not upgrade and keep them. Cypress published its reasons in a post of its own: in its account, the services traded on its brand and on an app whose costs Cypress carried.Knowing that parallel orchestration is a service with an owner, and keeping a fallback that needs nobody’s permission: specs split across machines by hand.
Cypress 14.4, 20 May 2025: Chrome 137 stops supporting --load-extension in branded ChromeThe @cypress/puppeteer plugin — the documented way to test several tabs — broke in open mode and headed runs in branded Chrome, as did launchOptions.extensions. Cypress recommended Electron, Chrome for Testing or Chromium; Cypress 16 later deprecated Electron as a test browser.CI images that pin a Chrome for Testing or Chromium build, rather than whatever branded Chrome the machine happens to have — a workaround for “one tab” rests on a browser flag the browser’s vendor owns.
Cypress 16, 1 September 2026: traffic intercepted on Chrome’s native network; cy.exec() and Cypress.env() removedA few cy.intercept() behaviours changed in Chrome-family browsers only — req.httpVersion is no longer reported, compression headers are gone from intercepted responses — and every test that read configuration in the browser had to choose between a browser-visible value and a Node-only one.The migration guide read before the upgrade, and the suite run in the browsers it targets — since the same test now crosses two network paths depending on the browser.

Where it does not help, and how it compares

Living inside the tab is a genuine architectural commitment, and every limit below follows from it. None is a scandal; each is worth knowing before choosing, because a later version will not remove it.

  • A cross-origin iframe — a payment form, an embedded sign-in — cannot be automated without switching off web security, and that works only in Chrome-family browsers.
  • One browser at a time, so two users acting together — a live chat, an agent watching a customer’s screen — is stubbed on one side rather than tested.
  • WebKit is experimental and arrives through Playwright’s WebKit build; it is not Safari either.
  • Parallelism is by spec file across machines, and balancing it well is a Cloud service.
  • Native dialogs, file pickers and anything at the level of the operating system are outside the page, and so outside reach.
  • cy.prompt(), in beta, writes steps from plain English and marks a step that recovered as “Self-healed”. A healed step is a changed test, and it deserves the review any other change gets.

Cypress is the better fit when

  • Front-end developers write and keep the tests, and the daily debugging experience decides whether they keep doing it.
  • Component testing in a real browser is a large part of the plan, not an afterthought.
  • The application lives on one origin, with no payment iframe, no second tab and no second user in a journey.
  • A team already has a working suite, where switching tools costs more than it returns.

Reach for Playwright when

  • The money path crosses a provider’s iframe and must run in more than one engine.
  • A journey needs two users at once, or crosses origins, tabs or windows as a matter of course.
  • Suite runtime is the constraint, and cheap parallelism on one machine changes the arithmetic.
  • A CI failure must be diagnosable from a file you own, without a hosted service.

Anti-patterns worth naming

  • Waiting a fixed number of milliseconds — the habit the retry model exists to make unnecessary.
  • Signing in through the interface at the start of every test, five seconds a case to prove what cy.session() proved once.
  • Conditional test logic based on what is on the page, which produces a test that cannot fail because it adapts to anything.
  • Stubbing every request until the suite tests no integration at all, then reporting it as end-to-end coverage.
  • One spec file per feature, so the longest feature sets the floor of every run.
  • chromeWebSecurity: false switched on for the whole suite to reach one iframe, so every journey quietly stops running in Firefox and WebKit.

When to use it

Use it when

  • Front-end teams who will keep the suite themselves and value the debugging experience every day.
  • Component testing in a real browser, moving cases down out of the slow end-to-end tier.
  • Web applications on one origin, without payment iframes, second tabs or two users in one journey.
  • Where stubbing the network at the boundary makes error and empty states testable that otherwise are not.

Avoid it when

  • Journeys that type into a third party’s iframe and must run in Firefox or WebKit.
  • Flows that legitimately cross tabs, windows or several origins, where the workarounds outnumber the tests.
  • Suites where total runtime is the binding constraint and a hosted balancing service is not an option.
  • Assertions written against application internals, which couple the test to how the feature is built.

Sources and method

What this page stands on

  • 11 sources
  • Checked October 19, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.