Home / Blog / Playwright vs Selenium
Playwright vs Selenium: what changes when you move a suite
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.0 | Playwright 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:
- Web only, on an engine Playwright drives, in a language it binds, and a red run gets re-run before anybody reads it. That suite is worth moving.
- Ruby, Kotlin, Safari, Internet Explorer, or a native-app half. That suite does not move, or moves in part and keeps a Selenium project alongside for the rest. Running two frameworks is a real cost, and it is cheaper to count it now than to meet it in month three.
- A suite small enough to run green in four minutes. Leave it alone. A migration buys you nothing you do not already have, and the cheapest suite you own is the one nobody is touching.
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.