Home / Blog / Playwright vs Selenium

Playwright vs Selenium: what changes when you move a suite

Quick answer

Playwright and Selenium both drive real browsers, and they differ in how. Selenium 4.47.0 sends each command over HTTP to a driver process. Playwright 1.62 holds one connection to the browser and checks that an element is visible, stable, enabled and receiving events before it acts. Playwright ships more tooling; Selenium reaches more browsers and more languages.

You already know Selenium, so this page does not explain what a driver is. It is about the mechanical part of leaving: which of your idioms survive the move, and which of your currently passing tests stop passing on the first run.

Samples here are written against Playwright 1.62 and Selenium 4.47.0. Where a sentence below says Selenium does something, it means that release. Selenium's documentation site labels its own pages v4.0 while its install snippets ship 4.47.0; both belong to the same 4.x line.

What is the difference between Playwright and Selenium?

Both drive a real browser and both are open source. The differences that change a working day sit in how a command reaches the browser, what happens before an action runs, and what arrives in the box with the library.

Selenium 4.47.0Playwright 1.62
Reaching the browser The client library talks over HTTP to a browser-specific driver process, which drives the browser. The bindings ship a configurable HTTP client with its own connection and read timeouts. One connection to the browser process. For Chromium the documentation describes it as a Chrome DevTools Protocol websocket.
Engines Anything with a WebDriver implementation, which today includes Chrome, Edge, Firefox, Safari and Internet Explorer. Chromium, Firefox and WebKit.
Branded browsers The branded browser is the target, driven by the vendor's own driver. Google Chrome and Microsoft Edge. Not branded Firefox, and not Safari: the WebKit build is not Safari and does not run on a real iOS device.
Official bindings Java, Python, C#, Ruby, JavaScript and Kotlin, where the Kotlin instructions are to use the Java bindings. JavaScript and TypeScript, Python, Java, .NET.
Waiting Explicit waits you write, with WebDriverWait and ExpectedConditions, plus an optional global implicit wait whose default is 0. Actionability checks before every action, and assertions that retry. There is no global implicit wait to set.
Isolation Selenium's own test-practice guidance is a new WebDriver for each test. A browser context per test, created by the runner.
Parallelism From your test framework — JUnit, TestNG, pytest-xdist — and from Grid across machines. Worker processes in the bundled runner, with fullyParallel in the config.
Runner in the box None. The documentation is direct about it: WebDriver "does not know a thing about testing". @playwright/test for JavaScript and TypeScript; a Pytest plugin for Python; JUnit or TestNG for Java; MSTest, NUnit and xUnit base classes for .NET.
Debugging artifact Driver logs, screenshots you take yourself, and BiDi or CDP logging. A trace.zip per test, opened with npx playwright show-trace.
Native mobile apps Not WebDriver's job on its own; teams pair it with Appium. No. Playwright drives browsers, and it does not drive an iOS or Android application.

The same login test, written twice. The Selenium half is Java because that is the language most existing suites are in.

package suite;

import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class LoginTest {
  @Test
  public void signsIn() {
    WebDriver driver = new ChromeDriver();
    try {
      driver.get("https://example.test/login");
      driver.findElement(By.id("username")).sendKeys("ada");
      driver.findElement(By.id("password")).sendKeys("correct horse");
      driver.findElement(By.xpath("//button[@type='submit']")).click();

      WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
      By greeting = By.cssSelector("[data-testid='greeting']");
      wait.until(ExpectedConditions.visibilityOfElementLocated(greeting));
      assertEquals("Welcome, Ada", driver.findElement(greeting).getText());
    } finally {
      driver.quit();
    }
  }
}
Selenium 4.47.0 · Java
import { test, expect } from '@playwright/test';

test('signs in', async ({ page }) => {
  await page.goto('https://example.test/login');
  await page.getByLabel('Username').fill('ada');
  await page.getByLabel('Password').fill('correct horse');
  await page.locator('xpath=//button[@type="submit"]').click();
  await expect(page.getByTestId('greeting')).toHaveText('Welcome, Ada');
});
Playwright 1.62 · TypeScript

What matters to a team with an existing suite is that the second version is recognisable. There is no browser setup and no teardown, because the runner owns the browser. There is no wait, because the assertion retries. And the XPath still runs: page.locator('xpath=//button') is a supported locator, and the documentation notes that "Any selector string starting with // or .. are assumed to be an xpath selector", so most of your existing strings work unchanged.

Whether that holds for your suite depends on what the XPath is pointed at. Playwright's documentation recommends user-visible locators over XPath tied to the implementation, and separately warns that "XPath does not pierce shadow roots". A suite of XPath against server-rendered HTML lands on the first afternoon. The same suite pointed at a shadow-DOM component library has to be rewritten selector by selector, and that is a rewrite rather than a migration.

Why is Playwright faster and less flaky than Selenium?

We have not measured this on a client suite, so this section names mechanisms and prints no multiplier. A benchmark that settled it would have to hold the application, the browser, the machine and the network constant across both runs, and report flake rate next to wall-clock time.

The connection

Selenium's client library sends each command as an HTTP request to a driver, which sits with the browser and passes the command on. That is why the bindings expose an HTTP client you can configure with connection and read timeouts, and why the driver is a separate executable you install. Playwright's documented route is a websocket that stays open: a browser started with launchServer publishes an endpoint, connect() attaches to it, and every command after that travels over the one connection. That is the path the documentation covers, and it does not cover the transport used when the runner launches the browser itself. How Playwright drives a browser goes long on that argument, including which parts of it are Chromium's alone.

What auto-waiting means, in full

In a feature table, auto-waiting is a tick in a box. In the documentation it is a specific list. The page opens: "Playwright performs a range of actionability checks on the elements before making actions to ensure these actions behave as expected." There are five checks: visible, stable, receives events, enabled and editable, and each action requires its own subset of them.

A locator.click() waits for the locator to resolve to exactly one element, and for that element to be visible, stable, receiving events and enabled. A locator.fill() waits for visible, enabled and editable, and does not care whether the element is stable. Reading that per-action table is how you find out which of your explicit waits were describing something the framework now does for you.

The numbers come from a different page, because the actionability page names no default timeout at all. Playwright's timeouts documentation gives a test timeout of 30 seconds and an assertion timeout of 5 seconds, both configurable. Neither of them is a global implicit wait.

Isolation, and what it costs on each side

Playwright's contexts documentation states the model plainly: "Playwright creates a context for each test, and provides a default Page in that context", and contexts are "fast and cheap to create and are completely isolated, even when running in a single browser."

Selenium 4 gets to the same place by a more expensive road, and its own documentation recommends taking it. The test-practices page on a fresh browser per test says: "If spinning up a new virtual machine is not practical, at least start a new WebDriver for each test." A new WebDriver is a new driver session and a new browser process. Teams that find that too slow share a driver across tests and pay for it in order-dependent failures, and Playwright does not put that trade in front of you.

What breaks when you move a Selenium suite to Playwright?

The move gets priced in a reader's head as a find-and-replace. Four Selenium idioms have no clean Playwright equivalent, and only one of the four is a line-for-line translation. Two of them are deletions: the code goes and nothing takes its place. That is cheaper than it sounds and harder to estimate, because what leaves is often the most-edited file in the repository.

The wait, and the thing underneath it

// Selenium 4.47.0
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement submit = wait.until(
    ExpectedConditions.elementToBeClickable(By.id("submit")));
submit.click();

// Playwright 1.62 — the same intent, in one line
await page.getByRole('button', { name: 'Submit' }).click();
Selenium 4.47.0 · Java, and Playwright 1.62 · TypeScript

That is the easy half, and it is why a first estimate comes back optimistic. The hard half is what those waits sit on top of. Selenium's own documentation carries this warning: "Do not mix implicit and explicit waits. Doing so can cause unpredictable wait times. For example, setting an implicit wait of 10 seconds and an explicit wait of 15 seconds could cause a timeout to occur after 20 seconds."

Large suites mix them, usually because the implicit wait was set once in a base class years ago and nobody has been brave enough to remove it. A suite in that state has no defined waiting behaviour to port. Before any of it can be translated faithfully, somebody has to work out what each test was waiting for, and some of those tests will turn out to have been passing on the mixture.

The four with no equivalent

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
Selenium 4.47.0 · Java

The global implicit wait. Nothing replaces it. Playwright has a test timeout, an assertion timeout and per-call timeout options, and no session-wide setting that makes every lookup retry. The line is deleted, and the behaviour it was propping up has to be found and named test by test. This is the idiom that moves an estimate.

WebElement row = driver.findElement(By.id("row-7"));
for (int attempt = 0; attempt < 3; attempt++) {
  try {
    row.click();
    break;
  } catch (StaleElementReferenceException e) {
    row = driver.findElement(By.id("row-7"));
  }
}
Selenium 4.47.0 · Java

The stale-element retry helper. Selenium's javadoc defines the exception as a reference to an element that "no longer appears on the DOM of the page": you held a handle, the page re-rendered, the handle died. Playwright has no counterpart, because a locator is a description resolved at the moment of use. "Every time a locator is used for an action, an up-to-date DOM element is located in the page." So the retry wrapper, the base-class catch block and the helper everybody imports all go. Read loops like the one above before you delete them. This one makes at most three click attempts: the first, then two retries. A test that was tuned to that number has a race in it, and the loop is the last place that number is written down.

public class CheckoutPage {
  @FindBy(id = "promo") private WebElement promoField;
  @FindBy(css = "[data-testid='total']") private WebElement total;

  public CheckoutPage(WebDriver driver) {
    PageFactory.initElements(driver, this);
  }
}
Selenium 4.47.0 · Java

@FindBy and PageFactory. No annotation-driven page factory exists in Playwright. PageFactory.initElements sets a lazy proxy behind each annotated field; a Playwright page object is a plain class holding locators, assigned in the constructor, with no framework doing anything to them afterwards. The line count barely changes. The habit changes: a Java team stops declaring elements and starts writing them, and the review comment "this should be a @FindBy" has nothing to become. Expect that to be the slowest part of the first month, and the part that never appears in an estimate.

new Actions(driver)
    .keyDown(Keys.SHIFT)
    .click(driver.findElement(By.id("item-4")))
    .keyUp(Keys.SHIFT)
    .perform();

((JavascriptExecutor) driver).executeScript(
    "window.myApp.use(arguments[0]);", payload);
Selenium 4.47.0 · Java

Actions chains and JavascriptExecutor. Here the answer is genuinely mixed, which is why it is the one to sample before quoting a number of days. Most gestures land on a named method: locator.hover(), locator.dragTo(), locator.press(), and a modifier click becomes locator.click({ modifiers: ['Shift'] }). Where a chain's granularity is the point, page.mouse.down(), page.mouse.move() and page.mouse.up() are there. JavascriptExecutor maps to page.evaluate(), with one difference that bites in review rather than at runtime: the test process and the page are separate environments which, in the documentation's words, "don't intersect", so a script that closed over a test variable has to take it as an argument. Escape hatches that read the DOM convert without much thought. Escape hatches written to work around a wait can go, because the reason they existed has gone.

Why do tests that passed in Selenium fail on the first Playwright run?

Because Playwright refuses an ambiguous selector and Selenium answers it. The documentation is explicit: "Locators are strict. This means that all operations on locators that imply some target DOM element will throw an exception if more than one element matches." Selenium's findElement has the opposite contract, and its javadoc says so in one line: "Find the first WebElement using the given method". A selector matching four things has always returned one of them, quietly.

<table>
  <tr><td>Ada</td><td><button class="danger">Delete</button></td></tr>
  <tr><td>Grace</td><td><button class="danger">Delete</button></td></tr>
</table>
The markup both of the next two snippets run against
// Selenium 4.47.0 — deletes Ada, says nothing about Grace
driver.findElement(By.cssSelector(".danger")).click();

// Playwright 1.62 — throws
await page.locator('.danger').click();
Selenium 4.47.0 · Java, and Playwright 1.62 · TypeScript
Error: locator.click: Error: strict mode violation: locator('.danger') resolved to 2 elements:
    1) <button class="danger">Delete</button> aka getByRole('row', { name: 'Ada Delete' }).getByRole('button')
    2) <button class="danger">Delete</button> aka getByRole('row', { name: 'Grace Delete' }).getByRole('button')

Call log:
  - waiting for locator('.danger')
Playwright 1.62 · the run output, reproduced against the markup above

Read what the error offers you. It lists both matches, and beside each one it prints the locator it would have used instead. The tests this hits were asserting against whichever element the DOM happened to put first, and nobody ever chose that order. Under Selenium that stays true until a sort order changes in production and a customer finds it. Under Playwright it surfaces in week one from a stack trace, at the price of a red suite on the day you switch.

.first() makes the error go away and keeps the ambiguity, which is occasionally right for a list where any row will do. Naming the element is the repair, and Playwright recommends seven locators for it: getByRole, getByText, getByLabel, getByPlaceholder, getByAltText, getByTitle and getByTestId. A converted suite ends up written in that vocabulary.

// The retreat: still ambiguous, now silently
await page.locator('.danger').first().click();

// The repair: names the row and the control
await page
  .getByRole('row', { name: 'Grace' })
  .getByRole('button', { name: 'Delete' })
  .click();
Playwright 1.62 · TypeScript

Where is Selenium still the better choice?

Selenium wins outright in several cases, and a team sitting in any of them should stay where it is.

Ruby and Kotlin have nowhere to go

Selenium 4.47.0 ships official bindings for Java, Python, C#, Ruby, JavaScript and Kotlin, and its Kotlin instructions are to use the Java bindings. Playwright ships four: JavaScript and TypeScript, Python, Java and .NET. A Ruby team looking at Playwright is looking at a wall. Rewriting the suite in another language is a different project from migrating it, with a different budget and different people, and it deserves to be argued as one.

Browsers past three engines

Playwright's documentation confirms it "can run tests on Chromium, WebKit and Firefox browsers as well as branded browsers such as Google Chrome and Microsoft Edge." Safari is where the distinction has to be exact, because "WebKit" in a browser column reads as Safari to most people. The documentation says both halves of it: Playwright's WebKit "is derived from the latest WebKit main branch sources, often before these updates are incorporated into Apple Safari and other WebKit-based browsers", and "Playwright doesn't work with the branded version of Safari since it relies on patches." Testing WebKit is not testing Safari, and it is certainly not testing Safari on a real iOS device. Internet Explorer does not appear on that documentation page at all, and Playwright does not support it. A compatibility matrix with Safari or IE on it is a requirement Playwright cannot meet and Selenium can.

A Grid that works, with people who run it

A functioning Grid with a capability matrix and somebody who understands it is an asset. Playwright replaces it with configuration and CI concurrency, which changes who owns the problem: you stop owning a Grid and start owning a worker count and a CI bill. That is worth doing for a team whose Grid is a source of tickets and worth nothing to a team whose Grid is quiet. If your Grid exists to generate traffic rather than to run browsers in parallel, Playwright does not replace it at all. Playwright measures one real browser doing one thing well; it is not a load tool and we do not sell it as one.

A bridge exists, with limits to check before you plan around it. Setting one environment variable points Playwright at a Selenium 4 Grid Hub.

SELENIUM_REMOTE_URL=http://<selenium-hub-ip>:4444 npx playwright test
Playwright 1.62 · Chrome and Edge only, Selenium 4, experimental

Playwright's documentation marks the integration experimental, says "Note that this only works for Google Chrome and Microsoft Edge", and records that Selenium 3 "is supported in a best-effort manner". It also states the risk itself: "There is a risk of Playwright integration with Selenium Grid Hub breaking in the future. Make sure you weight risks against benefits before using it." Treating that command as proof your Grid investment is safe is a bet nobody has the information to make.

Twenty years of answers

Selenium is much older than Playwright, and the corpus of solved obscure problems is much larger as a result. When something strange happens in a Selenium suite at six in the evening, the odds are good that somebody wrote the answer down years ago. That advantage shrinks a little every year. For a small team with no appetite for being the first person to hit a bug, it is a reason to stay, and it is not a permanent one.

Anything that reaches a native mobile app

Selenium paired with Appium drives an iOS or Android application. Playwright does not, and no configuration makes it. Playwright emulates a mobile browser: the viewport, the user agent and touch input. That covers mobile web and stops there. A suite with a native-app half keeps that half exactly where it is, whatever happens to the web half.

Should you switch from Selenium to Playwright?

"Better" needs a suite attached before it means anything. Playwright is better at the things Selenium leaves to you and worse at reach, so the trade is good or bad depending on which of the two you are currently paying for. The conditions are checkable in your own repository, without anybody's opinion:

If you have read this far and the answer for your suite is yes, the next question is what happens to the tests you already have, which is a job we do.

Questions

Is Playwright better than Selenium?

For a web-only suite in a language Playwright binds, on Chromium, Firefox or WebKit, Playwright removes work that Selenium 4.47.0 leaves to you: the explicit waits, the stale-element retries and the runner you had to choose. For a suite that needs Safari on macOS, Internet Explorer, Ruby or Kotlin, or that reaches a native mobile application through Appium, which Playwright cannot drive at all, Selenium is the only one of the two that can do the job. The question is which of those two descriptions matches the repository in front of you.

Is Playwright faster than Selenium?

We have not measured it on a client suite, so we print no multiplier. The mechanisms that make a difference are documented and checkable: Playwright talks to the browser over a persistent connection instead of a request per command, its browser contexts are cheaper to create than a fresh WebDriver session, and its runner parallelises across worker processes without a Grid. A benchmark that settled it would have to hold the application, the browser, the machine and the network constant, and report the flake rate alongside the wall-clock time, because a fast suite that gets re-run twice is not fast.

Does Playwright support Java, Python and Ruby?

Java, Python and .NET yes, alongside JavaScript and TypeScript. Ruby, no. Selenium 4.47.0 ships bindings for Java, Python, C#, Ruby, JavaScript and Kotlin, and its Kotlin instructions tell you to use the Java bindings; Playwright ships four. The Playwright documentation says all core browser automation features are supported in every language while the testing ecosystem integration differs: JavaScript and TypeScript get Playwright's own runner, Python uses the Pytest plugin, Java leaves the framework to you, and .NET ships base classes for MSTest, NUnit and xUnit.

Can Playwright use our existing Selenium Grid?

Partly, and the documentation calls it experimental. Setting SELENIUM_REMOTE_URL points Playwright at a Selenium 4 Grid Hub, and the docs say it only works for Google Chrome and Microsoft Edge. Selenium 3 is supported in a best-effort manner. Playwright connects over the Chrome DevTools Protocol websocket that Selenium 4 exposes, and the docs warn: "There is a risk of Playwright integration with Selenium Grid Hub breaking in the future. Make sure you weight risks against benefits before using it." It is a bridge for a transition, and it is not a reason to keep a Grid.

What to send us

If the decision has gone Playwright's way, the useful first call is one where we have already read the suite. The test count, the language it is written in and a link to a recent CI run are enough for us to come back with a shape for the work.