Playwright vs Cypress: An Honest Comparison for 2026
TL;DR
- Choose Playwright for cross-browser coverage including WebKit, multi-tab or multi-origin flows, WebSocket mocking, non-JS test languages (Python, Java, .NET), or free, self-managed parallel CI.
- Choose Cypress if your team values its in-browser, time-travel debugging, is already productive with it, or wants a hosted dashboard (Cypress Cloud) for flake detection and test replay.
- For network mocking, both are strong.
page.route()andcy.intercept()cover the same core cases. Playwright adds HAR replay and WebSocket routing. Cypress adds built-in delay, throttling and a cleaner alias-and-wait syntax.
Independence and method. devxtools.dev has no relationship with Microsoft (Playwright) or Cypress.io. The Playwright snippets on this page were run with Playwright 1.64.1 on Chromium, 2026-10-15. The Cypress snippets follow the official Cypress 16.2 docs and were not executed for this edition. Prices and download numbers were checked on 2026-10-15. We didn't run speed benchmarks, so this page doesn't claim either tool is faster.
At a glance
| Playwright | Cypress | |
|---|---|---|
| Licence | Apache-2.0 | MIT (app); Cypress Cloud is a paid SaaS with a free tier |
| Latest version (2026-10-15) | 1.64.1 | 16.2.0 |
| Test languages | JavaScript/TypeScript, Python, Java, .NET | JavaScript/TypeScript |
| Where the test runs | Node (or another language) process, driving the browser out of process | Inside the browser, next to your app |
| Browsers | Chromium, Firefox, WebKit | Chrome family (Chrome, Chromium, Edge), Firefox (WebDriver BiDi, Firefox 140+); WebKit is experimental; Electron is deprecated |
| Multiple tabs / browsers in one test | Yes | No (multi-tab only via the @cypress/puppeteer plugin) |
| Network mocking | page.route() / context.route(), routeFromHAR(), routeWebSocket() | cy.intercept(), with fixtures, delay and throttling |
| WebSocket mocking | Yes (routeWebSocket, since v1.48) | No frame-level interception |
| Parallel CI | Built-in workers and --shard, free | Needs --record to Cypress Cloud (free Starter plan includes parallelization, but it switches off after 500 test results/month until the cycle resets) |
| Debugging | Trace viewer, UI mode, codegen, HTML report | Interactive runner with time-travel snapshots; Test Replay in Cloud |
| Component testing | New framework-agnostic "stories" model (v1.62) | React, Angular, Vue, Svelte |
| GitHub stars (2026-10-15) | 97,773 | 51,041 |
| npm weekly downloads (2026-10-06 to 10-12) | @playwright/test ~76.2M; playwright ~113.6M | cypress ~5.95M |
Download counts include transitive installs, such as tools that depend on playwright. They show how widely each package is installed, not how many teams pick it for E2E testing.
Architecture: why it matters for your tests
Cypress runs your test code inside the browser, in the same run loop as your app. That's why its runner can show live DOM snapshots for every command and why cy.* chains feel synchronous. The trade-offs come from the same design, and Cypress lists them in its docs: one browser per test, no real multi-tab support, and work outside the browser (filesystem, database, Node APIs) goes through cy.task().
Playwright runs your test in a separate process and controls the browser over a protocol, so the test is ordinary async code (await page.click(...)). A single test can open several pages, contexts (isolated sessions) or browsers. That makes logins as two users, OAuth popups and multi-origin flows straightforward. The cost is that you debug through traces and the inspector rather than a live in-app runner.
For network control, Cypress 16 uses native CDP interception in Chrome, Chromium and Edge, and Firefox and WebKit still go through its legacy proxy. Playwright's routing API is the same across Chromium, Firefox and WebKit (with one documented exception: routing service-worker traffic is Chromium-only).
The same test in both
The scenario: the app fetches /api/users. We stub it and check the list renders.
Playwright (run on 2026-10-15, passed):
import { test, expect } from '@playwright/test';
test('mocks a GET request', async ({ page }) => {
await page.route('**/api/users*', async (route) => {
await route.fulfill({ json: [{ name: 'Ada Lovelace' }, { name: 'Alan Turing' }] });
});
await page.goto('/');
await expect(page.getByRole('listitem')).toHaveText(['Ada Lovelace', 'Alan Turing']);
});Cypress (from the docs, not executed):
it('mocks a GET request', () => {
cy.intercept(
{ method: 'GET', pathname: '/api/users' },
{ body: [{ name: 'Ada Lovelace' }, { name: 'Alan Turing' }] },
).as('getUsers');
cy.visit('/');
cy.wait('@getUsers');
cy.get('li').should('have.length', 2).first().should('have.text', 'Ada Lovelace');
});Both read cleanly. Cypress's .as() / cy.wait('@alias') makes "wait for this call" explicit. Playwright relies on auto-waiting assertions, and you can use page.waitForResponse() when you need the response itself.
Network control and API mocking (the deciding factor for many teams)
page.route() vs cy.intercept()
| Task | Playwright | Cypress |
|---|---|---|
| Stub JSON | route.fulfill({ json }) | cy.intercept(url, { body }) |
| Serve a fixture file | route.fulfill({ path: 'fixtures/users.json' }) | { fixture: 'users.json' } |
| Error status | route.fulfill({ status: 500 }) | { statusCode: 500 } |
| Network failure | route.abort('failed') (also 'timedout', etc.) | { forceNetworkError: true } (not on WebKit) |
| Delay / slow network | Your own await before fulfill | delay, throttleKbps built in |
| Modify the real response | route.fetch() then fulfill({ response, json }) | req.continue((res) => { res.body = … }) |
| Match only N times | { times: 1 } (v1.44+) | { times: 1 } in the route matcher |
| Precedence | Last registered handler runs first; route.fallback() passes on | Last defined route matches first; middleware: true routes run first |
| Lifetime | Per page/context (new for each test by default) | Cleared before each test |
| URL matching | Glob (* doesn't cross /, ** does, must match the whole URL), RegExp or a predicate (url) => boolean | minimatch glob with matchBase, RegExp, or a route-matcher object (pathname, query, method, …) |
| Gotcha | Service-worker traffic isn't seen unless you block service workers | ? is a wildcard in globs, so escape it as \\?. cy.request() calls aren't intercepted |
A failure-then-recovery test shows the APIs side by side.
Playwright (run on 2026-10-15, passed):
test('fail once, then succeed', async ({ page }) => {
await page.route('**/api/users*', (route) => route.fulfill({ json: [{ name: 'Recovered' }] }));
await page.route('**/api/users*', (route) => route.fulfill({ status: 503 }), { times: 1 });
await page.goto('/');
await expect(page.locator('#status')).toHaveText('Error 503');
await page.reload();
await expect(page.getByRole('listitem')).toHaveText(['Recovered']);
});Cypress (pattern from the cy.intercept docs, not executed):
it('fail once, then succeed', () => {
cy.intercept('GET', '/api/users', { body: [{ name: 'Recovered' }] });
cy.intercept({ method: 'GET', url: '/api/users', times: 1 }, { statusCode: 503 });
cy.visit('/');
cy.contains('Error 503');
cy.reload();
cy.contains('li', 'Recovered');
});HAR replay and WebSockets
Playwright can record and replay HAR files:
await page.routeFromHAR('hars/users.har', {
url: '**/api/**',
update: !!process.env.UPDATE_HAR, // record when UPDATE_HAR=1, replay otherwise
updateContent: 'embed',
});Matching is strict on URL and method (and on the body for POST requests). Unmatched requests are aborted by default, or passed through with notFound: 'fallback'. Cypress has no built-in HAR replay. You'd use fixtures or a plugin.
Playwright also mocks WebSockets with page.routeWebSocket(). cy.intercept() doesn't intercept WebSocket frames. If your app is real-time, that difference alone may decide it.
Using MSW with each
MSW lets you share one set of request handlers across unit tests, Storybook, local development and E2E.
- Playwright: the official
@msw/playwrightpackage (0.7.0) plugs your MSW handlers into a fixture. Under the hood it usespage.route()/context.route(), so no service worker is involved. We ran this on 2026-10-15:
// tests/msw.setup.ts
import { test as testBase } from '@playwright/test';
import type { AnyHandler } from 'msw';
import { defineNetworkFixture, type NetworkFixture } from '@msw/playwright';
import { handlers } from '../mocks/handlers';
export const test = testBase.extend<{ handlers: AnyHandler[]; network: NetworkFixture }>({
handlers: [handlers, { option: true }],
network: [async ({ context, handlers }, use) => {
const network = defineNetworkFixture({ context, handlers });
await network.enable();
await use(network);
await network.disable();
}, { auto: true }],
});Override per test with network.use(http.get('*/api/users', () => HttpResponse.json([], { status: 500 }))). Note that msw 3.x declares Node 22.12+ in its engines field and is ESM-only.
- Cypress: there's no official MSW adapter. The common pattern is to start MSW's browser worker inside the app under test (for example, in a dev or test build). A service worker that answers requests hides them from
cy.intercept(), so pick one mocking layer per request. In practice, many Cypress teams usecy.intercept()with fixtures for E2E and keep MSW for unit tests and Storybook.
Mock servers (framework-agnostic)
When the mock needs to be shared by several apps, stateful, or contract-driven, run a server and point both tools at it: Mockoon (desktop app plus CLI), WireMock (stateful scenarios, record/playback) or Prism (mocks from an OpenAPI spec, default port 4010). This works the same with Playwright and Cypress. Use a webServer entry in playwright.config.ts, or start the mock in CI before cypress run.
For a deep dive on the Playwright side, see How to mock APIs in Playwright.
Running in CI: parallelism and cost
Playwright: parallel workers on one machine are built in, and you split across machines with --shard:
# GitHub Actions matrix: 4 machines, no paid service
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}Merge the shard reports afterwards with npx playwright merge-reports. There's no hosted dashboard, so flake tracking and history are up to you or a third-party service.
Cypress: cypress run is free and open source, but parallelizing across machines requires recording to Cypress Cloud (cypress run --record --parallel). Cypress Cloud plans (checked 2026-10-15):
| Plan | Price | Includes |
|---|---|---|
| Starter | Free | 10 users, 500 test results/month, parallelization (disabled once the 500 results are used, until the cycle resets), load balancing, Test Replay |
| Team | $799/year ($67/month) | 50 users, 120K results/year, flake detection |
| Business | $3,199/year ($267/month) | Adds SSO, spec prioritization, auto-cancellation |
| Enterprise | Custom |
Once an organization hits its test-result limit, Cypress's FAQ says recorded runs still execute, but parallelization is disabled and new results are hidden from the dashboard until you upgrade or the usage cycle resets. The pricing page lists extra on-demand results at $6 per 1,000. A suite of 300 tests run 20 times a month is 6,000 results, which is well beyond the free tier. Do the maths for your own suite before you commit.
Debugging and developer experience
- Cypress: the interactive runner is still its standout feature. You hover over each command to see the DOM at that moment, and the network log sits next to it. Test Replay (Cloud) brings a similar experience to CI failures.
- Playwright: the trace viewer records actions, DOM snapshots, network and console for each test, and it's usually enabled on first retry in CI. UI mode (
npx playwright test --ui) adds watch mode and time-travel locally, and codegen (npx playwright codegen) records locators. Everything runs locally for free.
Both auto-wait and retry assertions. Playwright's getByRole-style locators and web-first assertions nudge you toward accessible selectors. Cypress commonly uses cy.get() with data-cy attributes or cy.contains().
Component testing
- Cypress Component Testing supports React, Angular, Vue and Svelte. It mounts components in a real browser with the same runner UI as E2E.
- Playwright replaced its old experimental
@playwright/experimental-ct-*packages in v1.62 with a new framework-agnostic "stories" model: you write stories, a gallery serves them, and tests use amountfixture. If you tried the experimental version before and gave up, look again, but expect a different setup.
If your component tests already live in Vitest or Jest with Testing Library, you may not need either.
Mobile testing
Neither tool tests native mobile apps. Both can emulate mobile viewports for responsive web: Playwright with device descriptors (viewport, user agent, touch), Cypress with cy.viewport(). Playwright can also run WebKit, the engine behind Safari, which is the closer check for iOS web. For native or React Native apps, use Detox (gray-box testing for React Native, with JS tests) or Maestro (YAML flows for Android and iOS).
Load testing is a different job
Neither tool is a load tester. For API load, use k6 or similar, and keep your browser suite focused on correctness.
Migrating from Cypress to Playwright (mapping table)
| Cypress | Playwright |
|---|---|
cy.visit('/') | await page.goto('/') |
cy.get('[data-cy=save]') | page.locator('[data-cy=save]') or page.getByTestId('save') (set testIdAttribute: 'data-cy') |
cy.contains('Save') | page.getByText('Save') |
.click() / .type('x') | await locator.click() / await locator.fill('x') |
.should('have.text', 'x') | await expect(locator).toHaveText('x') |
.should('be.visible') | await expect(locator).toBeVisible() |
cy.intercept(url, { body }) | await page.route(url, r => r.fulfill({ json })) |
cy.intercept(...).as('a'); cy.wait('@a') | const resp = page.waitForResponse(url); … await resp |
{ fixture: 'users.json' } | route.fulfill({ path: 'fixtures/users.json' }) |
{ forceNetworkError: true } | route.abort('failed') |
cy.request() | request fixture (await request.get(url)) |
cy.task() | Plain Node code in the test or a fixture |
beforeEach + custom commands | test.beforeEach + fixtures (test.extend) |
cy.session() | storageState (log in once in a setup project) |
cy.viewport(375, 667) | test.use({ viewport: { width: 375, height: 667 } }) or a device descriptor |
cypress run --record --parallel | npx playwright test --shard=1/4 (plus workers) |
The mental shift is from chained, implicitly queued commands to explicit await. Migrate a spec at a time. Both tools can run in the same CI pipeline during the switch.
Verdict
- Playwright is the safer default for new projects that need broad browser coverage (especially WebKit), multi-page flows, WebSocket or HAR-based mocking, free self-hosted parallelism, or non-JavaScript languages.
- Cypress remains a good choice for teams that value its interactive runner and Cloud dashboard, test mostly in Chrome-family browsers, and don't hit its architectural limits (multiple tabs, WebSockets, WebKit). If your Cypress suite is stable, migrating costs real time. Do it because of a specific limitation you hit, not because of a trend.
Still undecided? Use the tool picker, browse the testing hub, or see all comparisons.
FAQ
Is Playwright better than Cypress?
Neither is better across the board. Playwright is the stronger fit if you need WebKit, multiple tabs or origins in one test, WebSocket or HAR mocking, languages beyond JS/TS, or free cross-machine parallelism. Cypress is the stronger fit if your team relies on its interactive time-travel runner and Cypress Cloud's dashboard, and mostly tests in Chrome-family browsers. Pick based on those constraints, not on popularity.
Is Playwright faster than Cypress?
We didn't benchmark them, and published benchmarks vary a lot with suite design. Playwright's free sharding and multi-worker runs often reduce wall-clock time in CI without a paid service. Measure on your own suite.
Can Cypress test Safari / WebKit?
Only experimentally. Cypress's WebKit support needs experimentalWebKitSupport and the playwright-webkit package, and it lacks some features (for example cy.origin, Test Replay and forceNetworkError). Playwright supports WebKit as a first-class browser.
Which is better for API mocking?
For HTTP stubbing they're roughly equal. Playwright also replays HAR files and mocks WebSockets. Cypress has built-in delay/throttle and alias-based waiting. For shared mocks across tools, use MSW or a mock server.
Does Playwright need a paid service for parallel CI?
No. Workers and --shard are built in and free. Cypress needs Cypress Cloud for cross-machine parallelization, and its free Starter plan includes it only until you reach 500 test results in a month. After that, parallelization is disabled until the cycle resets.
Can I use MSW with Playwright and Cypress?
Yes. With Playwright, use the official @msw/playwright fixture. It works through page.route(), so no service worker is needed. With Cypress, run MSW's browser worker in the app, or keep MSW for unit tests and use cy.intercept() for E2E.
Should I migrate from Cypress to Playwright?
Only if you hit a concrete limitation (WebKit, multi-tab, WebSockets, CI cost or language). Use the mapping table above and migrate spec by spec.
Related
- How to mock APIs in Playwright
- Playwright · Cypress · MSW · Mockoon · WireMock · Prism
- Detox · Maestro · k6
- Testing hub · All comparisons · Tool picker
Sources (checked 2026-10-15)
- Playwright release notes (1.44
times, 1.48routeWebSocket, 1.62 component testing): https://playwright.dev/docs/release-notes - Playwright
Route/Page.route/BrowserContext.route/routeFromHAR: https://playwright.dev/docs/api/class-route · https://playwright.dev/docs/api/class-page · https://playwright.dev/docs/api/class-browsercontext · https://playwright.dev/docs/mock - Playwright service workers: https://playwright.dev/docs/service-workers · sharding: https://playwright.dev/docs/test-sharding · languages: https://playwright.dev/docs/languages · component testing: https://playwright.dev/docs/test-components
- Playwright repo (Apache-2.0, stars, 1.64.1): https://github.com/microsoft/playwright
- Cypress
cy.intercept: https://docs.cypress.io/api/commands/intercept - Cypress browsers (Firefox BiDi, WebKit experimental, Electron deprecation): https://docs.cypress.io/app/references/launching-browsers
- Cypress trade-offs: https://docs.cypress.io/app/references/trade-offs
- Cypress parallelization: https://docs.cypress.io/cloud/features/smart-orchestration/parallelization
- Cypress Cloud pricing: https://www.cypress.io/pricing · test-result limit behaviour: https://docs.cypress.io/cloud/faq
- Cypress repo (MIT, stars, 16.2.0): https://github.com/cypress-io/cypress
@msw/playwright: https://github.com/mswjs/playwright · MSW: https://mswjs.io/ · https://github.com/mswjs/msw- npm downloads: https://api.npmjs.org/downloads/point/last-week/@playwright/test (also
playwright,cypress) - Our runs: Playwright 1.64.1 (Chromium), msw 3.0.3, @msw/playwright 0.7.0, Node 22.23.3, Linux, 2026-10-15 (17/17 tests passed)