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 APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms 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.
| # | Journey | In Cypress | Verdict |
|---|---|---|---|
| 1 | Search “blue jumper”, open SKU 30417, pay with the test card | The 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 only | Workaround · Chrome-family only |
| 2 | Sign up, confirm, sign in, reload — in Chromium and WebKit | WebKit runs only behind experimentalWebKitSupport, with Playwright’s WebKit build installed as playwright-webkit | Workaround · experimental |
| 3 | Reset a forgotten password from the emailed link | cy.task() reads the test inbox in Node; cy.visit() opens the link | Native |
| 4 | Apply SPRING10, change the quantity, pay | The same card iframe as journey 1; cy.request() reads the charge from the sandbox | Workaround · Chrome-family only |
| 5 | Pay with 3-D Secure: out to the bank’s check and back | The card iframe, and then the bank’s page on another origin: its steps go inside a cy.origin() block, which receives values only through args | Workaround · two of them |
| 6 | A card is declined; pay again | The card iframe; cy.request() counts the orders afterwards | Workaround · Chrome-family only |
| 7 | Ask 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 sends | Native |
| 8 | Start a return on day 30; the label arrives by email | cy.clock() sets the browser’s date; the server’s test clock is set through cy.request() | Native |
| 9 | The warehouse confirms the return; the refund lands | cy.request() posts the warehouse’s webhook from Node, where CORS does not apply; cy.task() polls the inbox | Native |
| 10 | Change the delivery address before dispatch | cy.request() reads the warehouse’s test endpoint | Native |
| 11 | Check out as a guest, then create an account | Test isolation clears the page, cookies and both web storages before each test — though not IndexedDB | Native |
| 12 | A support agent refunds a damaged item in the back office | cy.session() signs the agent in once and restores that session for every test that asks for it | Native |
A team can avoid the iframea page inside a pageAn HTML element that embeds another document inside a page — a payment form, a video, a sign-in box. When it comes from another origin, the browser walls it off from the page around it, which is why a test tool may or may not be able to type into it.All terms 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 event loopone thread taking turnsA loop on a single thread that takes finished work from a queue — a reply that has arrived, a timer that has fired — and runs its callback, one at a time. It lets one thread juggle thousands of waits, and it stops for everyone while any single callback runs.Full article →All terms. 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().
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 HTTPHyperText Transfer Protocol — the protocol of the webThe protocol browsers and servers talk in: the client sends a method and an address, the server answers with a status code and a body. It keeps no memory between requests — every request stands alone, which is why anything that must be remembered is carried explicitly.All terms/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-off | What it means | In 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 sites | Fits: 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 tabs | Journey 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 origin | The 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.
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.
| What you wrote | What retries | What to do |
|---|---|---|
| Queries, then an assertion | The whole chain of queries, from the top | Nothing — this is the shape the model is built for |
| Queries, an assertion, more queries, an assertion | Up to the first assertion; after it, only from the locked subject | Split 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 once | Assert the state the action produced, not the action |
A value taken in .then() and checked later | Nothing: the value is frozen when it is read | Assert on the live element instead |
cy.getCookie() and storage reads | Since Cypress 16, they are queries and retry too | Chain 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 browser contextan isolated profile in one browserAn isolated session inside one running browser, with its own cookies, storage and permissions, as Playwright uses it. Opening one takes milliseconds rather than a browser launch, so each test can start as a fresh user, and two contexts in one test are two users who cannot see each other.Full article →All terms, 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.
| Need | Command | Runs in | Worth knowing |
|---|---|---|---|
| Sign in once, reuse it | cy.session() | The browser | Caches and restores cookies and both web storages under an id; validate checks a restored session still works (journey 12) |
| Call an API | cy.request() | Node, on Cypress’s side | Not bound by CORS or the same-origin policy, which is why it can post the warehouse’s webhook (journey 9) |
| Anything else outside the browser | cy.task() | Node, in your config’s setupNodeEvents | Reading the inbox, seeding the database. cy.exec(), which ran shell commands, was removed in Cypress 16 in its favour |
| A configuration value | Cypress.expose() or cy.env() | The browser, or Node | Cypress 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.
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.
| Change | What it exposed | What protected a suite |
|---|---|---|
| Cypress 12, 6 December 2022: test isolation on by default; queries retry as a chain | Tests 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 v12 | Projects 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 Chrome | The @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() removed | A 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: falseswitched 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
The tool, from its own documentation
the verdicts, the trade-offs, the queue, retry-ability, isolation and parallelism in sections 1–6- DocumentationTrade-offs in Cypress (opens in a new tab)Cypress
- DocumentationCross origin testing in Cypress (opens in a new tab)Cypress
- DocumentationIntroduction to Cypress (opens in a new tab)Cypress
- DocumentationRetry-ability in Cypress (opens in a new tab)Cypress
- DocumentationTest isolation in Cypress (opens in a new tab)Cypress
- Documentationcy.origin() (opens in a new tab)Cypress
- Documentationcy.session() (opens in a new tab)Cypress
- DocumentationParallelize tests in Cypress Cloud (opens in a new tab)Cypress
- DocumentationDebug failed and flaky tests with test replay in Cypress Cloud (opens in a new tab)Cypress
The changes
the four rows of “Four changes underneath working suites”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.