Toolmingo
Guides12 min read

Cypress vs Playwright: Which E2E Testing Tool in 2025?

An in-depth comparison of Cypress and Playwright — covering speed, browser support, API, parallelisation, debugging, CI/CD integration, and when to choose each for your test suite.

Cypress dominated end-to-end testing for years. Playwright, released by Microsoft in 2020, caught up fast — and in many benchmarks now surpasses it. The key difference: Cypress runs inside the browser; Playwright controls browsers from outside via the Chrome DevTools Protocol and WebDriver BiDi. This guide tells you exactly what that means in practice and which to pick.

At a glance

Playwright Cypress
Vendor Microsoft Cypress.io
Released 2020 2014
Language support JS/TS, Python, Java, C# JS/TS only
Browser support Chromium, Firefox, WebKit (Safari) Chrome, Edge, Firefox (no WebKit)
Architecture Out-of-process (CDP + WebDriver BiDi) In-browser (iframe + proxy)
Parallelisation Built-in (free) Requires Cypress Cloud (paid)
GitHub stars (2025) ~65k ~47k
npm weekly downloads ~10M ~6M
Auto-wait Yes Yes
Test isolation Per-test browser context Per-test (shared origin cookies by default)

How Playwright works

Playwright runs as a Node.js process outside the browser. It communicates with Chromium, Firefox, and WebKit via the Chrome DevTools Protocol (CDP) and the newer WebDriver BiDi standard. Each test gets its own BrowserContext — an isolated browser session with separate cookies, localStorage, and cache — without the cost of launching a full browser.

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: "html",
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
  },
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox",  use: { ...devices["Desktop Firefox"] } },
    { name: "webkit",   use: { ...devices["Desktop Safari"] } },
  ],
});

How Cypress works

Cypress runs inside the browser in an iframe alongside your application. This gives it direct DOM access and synchronous JavaScript execution, but limits it to one browser tab, one origin (by default), and makes network stubbing work via a Node.js proxy rather than native browser interception.

// cypress.config.js
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    setupNodeEvents(on, config) {},
    specPattern: "cypress/e2e/**/*.cy.{js,ts}",
    video: true,
    screenshotOnRunFailure: true,
  },
});

Writing the same test: side by side

Scenario: Log in, navigate to dashboard, assert a welcome message.

Playwright

// tests/login.spec.ts
import { test, expect } from "@playwright/test";

test.beforeEach(async ({ page }) => {
  await page.goto("/login");
});

test("user can log in and sees dashboard", async ({ page }) => {
  await page.getByLabel("Email").fill("user@example.com");
  await page.getByLabel("Password").fill("secret123");
  await page.getByRole("button", { name: "Sign in" }).click();

  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByRole("heading", { name: /welcome/i })).toBeVisible();
});

test("shows error for wrong password", async ({ page }) => {
  await page.getByLabel("Email").fill("user@example.com");
  await page.getByLabel("Password").fill("wrong");
  await page.getByRole("button", { name: "Sign in" }).click();

  await expect(page.getByText("Invalid credentials")).toBeVisible();
});

Cypress

// cypress/e2e/login.cy.ts
describe("Login", () => {
  beforeEach(() => {
    cy.visit("/login");
  });

  it("user can log in and sees dashboard", () => {
    cy.get('input[name="email"]').type("user@example.com");
    cy.get('input[name="password"]').type("secret123");
    cy.get('button[type="submit"]').click();

    cy.url().should("include", "/dashboard");
    cy.get("h1").should("contain.text", "Welcome");
  });

  it("shows error for wrong password", () => {
    cy.get('input[name="email"]').type("user@example.com");
    cy.get('input[name="password"]').type("wrong");
    cy.get('button[type="submit"]').click();

    cy.contains("Invalid credentials").should("be.visible");
  });
});

Key syntax differences:

  • Playwright uses await (real async); Cypress uses a custom command queue
  • Playwright prefers semantic locators (getByLabel, getByRole); Cypress uses CSS selectors by convention
  • Playwright assertions use expect(locator).toBeVisible(); Cypress chains .should()

Performance comparison

Scenario Playwright Cypress
Cold start ~2s ~4s
Per-test browser context ~50ms ~200ms (page reload)
Parallel execution Free, multi-worker Paid (Cypress Cloud) or manual sharding
100-test suite (serial) ~3 min ~5 min
100-test suite (parallel, 4 workers) ~55s ~55s (Cloud) / ~5 min (free)
Memory per worker ~80 MB ~200 MB
CI cold start overhead Low (headless default) Medium (Electron default)

API comparison

Feature Playwright Cypress
Locator strategy getByRole, getByLabel, getByText, locator() cy.get() CSS/XPath, cy.contains(), Testing Library plugin
Auto-wait timeout Configurable (default 30s) Configurable (default 4s per assertion)
Network interception page.route() — intercepts at browser level cy.intercept() — via Node proxy
Multi-tab / popup Native (context.newPage(), page.waitForPopup()) Limited (single tab per test, experimental multi-origin)
iframes frame() / frameLocator() cy.iframe() (plugin) or cy.get('iframe').its('0.contentDocument')
File download page.waitForEvent('download') cy.readFile() after configuring download path
File upload locator.setInputFiles() cy.get('input').selectFile()
Visual comparison expect(page).toHaveScreenshot() (built-in) cy.matchImageSnapshot() (plugin)
Accessibility testing @axe-core/playwright cypress-axe plugin
API testing request.get/post/put/delete (built-in) cy.request() (built-in)
Component testing @playwright/experimental-ct-* First-class (React, Vue, Angular, Svelte)
Code generation npx playwright codegen Cypress Studio (experimental)
Test reporter HTML, JSON, JUnit, Allure Mochawesome, JUnit, Cypress Dashboard

Browser support

Browser Playwright Cypress
Chrome / Chromium
Edge (Chromium)
Firefox ✅ (limited devtools)
WebKit (Safari engine)
Safari (real) macOS only
Mobile emulation ✅ (Chromium + WebKit) ✅ (viewport only, no real mobile)
Real device testing Via cloud providers Via cloud providers

WebKit support is the clearest Playwright advantage — you can catch Safari-specific bugs without owning a Mac.


Where Playwright wins

Scenario Why Playwright wins
Cross-browser testing WebKit support catches Safari bugs in CI
Large test suites Free parallelisation across workers
Multi-language teams Python, Java, C# SDKs available
Multi-tab / multi-origin Native support with no hacks
Performance-critical CI Faster per-test isolation via BrowserContext
Tracing & debugging Built-in Trace Viewer with timeline, network, snapshots
API + UI testing APIRequestContext built in, no extra setup
Open source cost Fully free parallel testing
Mobile web Real WebKit emulation
Newer architecture WebDriver BiDi future-proofs the tool

Where Cypress wins

Scenario Why Cypress wins
Component testing Mature, first-class support for React/Vue/Angular/Svelte
Developer UX Time-travel debugger, visual test runner in browser
Learning curve Simpler mental model for small teams, no async awareness needed
Error messages Excellent human-readable failure messages
Community plugins Larger plugin ecosystem (cypress-axe, cypress-testing-library, etc.)
JavaScript-only teams Single language, no context switching
Stubbing/mocking Simpler cy.intercept() API for common patterns
Mature docs Deeper guides, recipes, and examples
Real-time reloads Watch mode rebuilds instantly in the interactive runner
Migration from older tools Easiest Selenium migration path for JS teams

Project setup comparison

Playwright setup

npm init playwright@latest
# Installs browsers, creates playwright.config.ts, example tests
npx playwright test              # run all tests
npx playwright test --ui         # open interactive UI mode
npx playwright codegen           # record a test
npx playwright show-report       # open HTML report

Cypress setup

npm install cypress --save-dev
npx cypress open                 # interactive test runner (browser)
npx cypress run                  # headless run
npx cypress run --spec "cypress/e2e/login.cy.ts"

CI/CD integration

Playwright (GitHub Actions)

# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20" }
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

Cypress (GitHub Actions)

# .github/workflows/cypress.yml
name: Cypress Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cypress-io/github-action@v6
        with:
          build: npm run build
          start: npm start
          wait-on: "http://localhost:3000"
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: cypress-screenshots
          path: cypress/screenshots

Network mocking comparison

Both tools let you intercept HTTP requests, but the implementation differs.

Playwright

test("shows cached products", async ({ page }) => {
  // Intercept at browser level — no proxy needed
  await page.route("**/api/products", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify([{ id: 1, name: "Laptop", price: 999 }]),
    });
  });

  await page.goto("/products");
  await expect(page.getByText("Laptop")).toBeVisible();
});

Cypress

it("shows cached products", () => {
  cy.intercept("GET", "/api/products", {
    statusCode: 200,
    body: [{ id: 1, name: "Laptop", price: 999 }],
  }).as("getProducts");

  cy.visit("/products");
  cy.wait("@getProducts");
  cy.contains("Laptop").should("be.visible");
});

Debugging experience

Feature Playwright Cypress
Trace Viewer ✅ Timeline, DOM snapshots, network waterfall
Time-travel debugging Via Trace Viewer (post-run) ✅ Live in interactive runner
Screenshot on failure ✅ Auto ✅ Auto
Video on failure
--debug mode Inspector pauses test for step-through cy.debug() / cy.pause()
VS Code extension ✅ (run/debug tests in editor) ✅ (Cypress extension)
Network panel In Trace Viewer In DevTools (interactive runner)

Full comparison

Feature Playwright Cypress
Architecture Out-of-process In-browser
Languages JS/TS, Python, Java, C# JS/TS only
Browser support Chromium, Firefox, WebKit Chrome, Edge, Firefox
Parallelisation Free built-in Paid (Cypress Cloud)
Component testing Experimental First-class
Auto-wait Yes Yes
Network intercept Browser-level (CDP) Proxy-level
Multi-tab Yes No (workaround needed)
Multi-origin Yes Limited (cy.origin)
iframes frameLocator() Plugin or workaround
Visual testing Built-in screenshots Plugin
API testing Built-in APIRequestContext cy.request()
Trace / replay Full trace viewer Time-travel in runner
Mobile emulation Real WebKit engine Viewport resize only
Code generation playwright codegen Cypress Studio
Price for parallelism Free Cypress Cloud subscription
Learning curve Moderate (async/await) Gentle (queued commands)
GitHub stars (2025) ~65k ~47k
Community / plugins Growing Mature
Corporate backing Microsoft Cypress.io (VC-backed)

Migration: Cypress → Playwright

Cypress Playwright equivalent
cy.visit(url) await page.goto(url)
cy.get('button') page.locator('button')
cy.contains('text') page.getByText('text')
cy.get('input[name="email"]') page.getByLabel('Email') or page.locator('...')
cy.click() await locator.click()
cy.type('hello') await locator.fill('hello')
cy.should('be.visible') await expect(locator).toBeVisible()
cy.url().should('include', '/path') await expect(page).toHaveURL(/path/)
cy.intercept(method, url, body) await page.route(url, route => route.fulfill(...))
cy.wait('@alias') await page.waitForResponse(url)
cy.request(url) await request.get(url)
beforeEach(() => cy.visit()) test.beforeEach(async ({ page }) => await page.goto())

Decision guide

Do you need Safari/WebKit testing?
├── Yes → Playwright

Do you need multi-tab or multi-origin tests?
├── Yes → Playwright

Is your team using Python, Java, or C#?
├── Yes → Playwright

Do you want free parallelisation?
├── Yes → Playwright

Is component testing a priority?
├── Yes → Cypress (more mature)

Does your team prefer a visual, in-browser runner for daily development?
├── Yes → Cypress

Are you migrating from Selenium with a JS/TS team?
├── Yes → Cypress (easier conceptual jump)

Are you starting fresh with a modern JS/TS project?
├── → Playwright (default recommendation 2025)

Common mistakes

Mistake Problem Fix
Not using getByRole/getByLabel in Playwright Brittle CSS selectors break on UI refactors Use semantic locators; add data-testid sparingly
Forgetting await in Playwright Silent failures — test passes without actually waiting Use TypeScript to catch missing await at compile time
Mixing Cypress queue and async/await Cypress commands aren't real promises; using async/await incorrectly breaks the queue Stick to Cypress chaining OR use cy.then() for async work
Not setting baseURL Hardcoded URLs make tests fail in different environments Configure baseURL in playwright.config.ts / cypress.config.js
Shared state between Cypress tests Default cy.session() or persistent cookies cause test order dependency Use cy.clearCookies() / cy.clearLocalStorage() in beforeEach
Running Playwright in serial on CI Wasted parallelisation opportunity Set fullyParallel: true and use multiple workers
Ignoring Playwright Trace Viewer Debugging CI failures blind Always enable trace: 'on-first-retry' in CI config
Skipping flaky test analysis Flaky tests erode trust in the whole suite Use Playwright's --retries + --reporter=list to identify and quarantine flaky tests

Cypress vs Playwright vs Selenium vs WebdriverIO

Cypress Playwright Selenium WebdriverIO
Architecture In-browser Out-of-process CDP WebDriver protocol WebDriver + DevTools
Languages JS/TS JS/TS, Python, Java, C# All major JS/TS
Safari/WebKit ✅ (via SafariDriver)
Speed Fast Fastest Slowest Fast
Parallelism Paid Free built-in Manual Built-in
Component tests ✅ First-class Experimental
Maturity 2014 (Mature) 2020 (Fast-growing) 2004 (Legacy) 2015 (Mature)
Learning curve Easy Moderate Steep Moderate
Best for JS teams, component + E2E Cross-browser, CI/CD, multi-lang Legacy projects WebDriver + DevTools hybrid

FAQ

Is Playwright replacing Cypress? Playwright has overtaken Cypress in npm downloads and GitHub stars in 2025. For new projects — especially cross-browser or multilingual teams — Playwright is now the default recommendation. Cypress remains the better choice for component testing and teams that value its interactive runner.

Can I use both Playwright and Cypress in the same project? Yes, though it's unusual. Some teams run Playwright for E2E cross-browser tests and Cypress for component tests (React/Vue). The maintenance overhead doubles, so only do this if the tradeoff is worth it.

Which is faster — Cypress or Playwright? Playwright is faster in CI, mainly because: (1) free parallelisation across workers, (2) lighter per-test BrowserContext vs full page reload, (3) headless Chromium default. For a 100-test suite, Playwright typically runs 30–50% faster with 4 workers vs free-tier Cypress.

Does Playwright work with React Testing Library? Not directly — RTL is a unit/component testing tool. Playwright has @playwright/experimental-ct-react for component tests, but for E2E, you write tests against the full running app. Use semantic locators (getByRole, getByLabel) which share the same accessibility-first philosophy as RTL.

Which should I learn first? If you're learning E2E testing for the first time: Playwright. Its modern async/await API matches how JavaScript works, the docs are excellent, and it's where the industry is heading. Cypress's queued command model is a non-standard mental model you'll have to unlearn later.

Does Playwright support mobile testing? Playwright supports mobile emulation (WebKit + Chromium with device profiles) for web tests. It does not test native iOS/Android apps. For real device mobile web testing, use cloud providers like BrowserStack or Sauce Labs with Playwright's remote connection support.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools