Selenium vs Puppeteer vs Playwright

Selenium vs Puppeteer vs Playwright: a side-by-side comparison

Selenium, Puppeteer, and Playwright are the three mainstream browser-automation libraries in 2026. Selenium speaks W3C WebDriver; Puppeteer and Playwright both speak Chrome DevTools Protocol. LumaBrowser drives all three from one browser — WebDriver on port 9515, a shared CDP server on port 9222 — and routes every natural-language selector call through the same LLM fallback orchestrator. Choosing between them is about ergonomics, not capability.

How the three drivers fit together

LumaBrowser exposes two servers and three supported clients. Puppeteer and Playwright both speak CDP, so they share a server:

                    ┌───────────────────────────── LumaBrowser ─────────────────────────────┐
                    │                                                                       │
  selenium-driver   │    Selenium WebDriver server  ──▶ port 9515  (W3C WebDriver / HTTP)   │
                    │                                                                       │
                    │                                              ┌─▶ Puppeteer clients    │
  cdp-driver        │    CDP WebSocket server       ──▶ port 9222 ─┤                        │
                    │    (shared endpoint)                         └─▶ Playwright           │
                    │                                                  (connectOverCDP)     │
                    │                                                  & Playwright MCP      │
                    │                                                                       │
                    │                ┌──────────────── Lumabyte fallback orchestrator ──┐   │
                    │                │  deterministic match → LLM attempt → validation  │   │
                    │                │         (shared by all three protocol paths)     │   │
                    │                └──────────────────────────────────────────────────┘   │
                    └───────────────────────────────────────────────────────────────────────┘

The CDP server handles Puppeteer and Playwright as two clients of the same endpoint — that’s why puppeteer.connect({ browserURL: 'http://127.0.0.1:9222' }) and chromium.connectOverCDP('http://127.0.0.1:9222') both work, and both pick up the Lumabyte.* custom domain for free.

What they share

Before we dive into differences, the things that are not a tradeoff:

  • The same LLM selector fallback. Natural-language element resolution (ai-description in Selenium, Lumabyte.find/Lumabyte.click for Puppeteer and Playwright) routes through one shared orchestrator: deterministic accessibility match → LLM attempt → validation → background shadow-template regeneration. Improvements to the pipeline ship to all three drivers at once.
  • The same LLM slot. All three drivers call the core selector-resolver slot. Configure a fast, low-latency model once and every description-based call from any client benefits.
  • The same DOM-snapshot shortcut. Single-round-trip page capture: POST /lumabyte/dom/snapshot in Selenium, Lumabyte.domSnapshot via CDP for Puppeteer and Playwright.
  • The same MCP control surface. Start, stop, and inspect either driver from Claude or any MCP client using the selenium_driver_* or cdp_driver_* tools.
  • The same host process. Every driver lives in-process with the browser — no ChromeDriver binary, no bundled Chromium child, no version drift.
Side-by-side
Dimension Selenium Puppeteer Playwright
LumaBrowser extension selenium-driver cdp-driver (shared)
Wire protocol W3C WebDriver Level 2 (HTTP) Chrome DevTools Protocol (WebSocket)
Default port 9515 (ChromeDriver) 9222 (Chrome DevTools)
Connect webdriver.Remote("http://127.0.0.1:9515") puppeteer.connect({ browserURL: "http://127.0.0.1:9222" }) chromium.connectOverCDP("http://127.0.0.1:9222")
Wire format REST: verb + URL + JSON body JSON-RPC: { id, sessionId?, method, params }
Events Poll. No native event stream; clients re-query on an interval. Push. Chromium streams Page.*, Network.*, Console.* over the same WebSocket.
Request interception Not W3C-standard; reach for the /lumabyte/cdp/execute passthrough. Native. Fetch domain or page.route — pause, modify, fulfill.
Locator idioms CSS, XPath, link-text, ai-description vendor strategy. CSS via page.$, XPath, raw DOM.querySelector, Lumabyte.find. Modern locators: getByRole, getByTestId, getByText, auto-wait assertions, Lumabyte.find.
Test assertions Bring your own (pytest, junit, etc.). Bring your own. Built-in web-first expect(locator).toBeVisible() style.
Natural-language elements ai-description locator strategy; /lumabyte/find, /lumabyte/click vendor endpoints. Lumabyte.find({ description }), Lumabyte.click({ description }) via CDP session. Same Lumabyte.* domain, dispatched via context.newCDPSession(page).send(...).
Fallback configuration Per-session capability lumabyte:llmFallback. Per-CDP-session method call Lumabyte.configureFallback({ enabled, ... }).
Tab ownership Drives any tab. Operates on whichever tab is active, including tabs the user is interacting with. Dedicated tabs. Spawns cdp-kind tabs with a purple accent and CDP badge. User tabs stay invisible to the CDP client.
Language ecosystem Every mainstream language: Python, Java, C#, Ruby, JS, Kotlin, Go, Rust, PHP. JS/TS-first. CDP clients exist for Python, Go, Rust. JS/TS, Python, .NET, Java — all use connectOverCDP.
MCP agent story No first-class MCP server from the Selenium project. Ecosystem MCP servers exist (browser-use, AgentCP). @playwright/mcp supports --cdp-endpoint http://127.0.0.1:9222 — the 2026 default.
Stealth / fingerprinting Higher signal. WebDriver conformance sets navigator.webdriver. Lower signal. navigator.webdriver is not set by CDP. Emulation.* and Network.setUserAgentOverride are native.
Selenium Grid compatibility Yes. Set URL prefix to /wd/hub in settings. Not applicable — CDP doesn’t participate in Grid.
Known limits Frame switching, shadow-DOM subqueries return unsupported operation. Use the CDP passthrough as escape hatch. None specific to LumaBrowser. Firefox/WebKit unreachable (connectOverCDP is Chromium-only); tracing partial; codegen needs a launched Chromium.
Connecting — the shape of a session

Same browser, same LumaBrowser instance, three different handshakes:

Selenium — WebDriver on 9515
# pip install selenium
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Remote(
    "http://127.0.0.1:9515",
    options=webdriver.ChromeOptions(),
)

# Operates on whichever tab is active
# in LumaBrowser.
driver.get("https://example.com")
title = driver.find_element(
    By.CSS_SELECTOR, "h1",
).text
print(title)

driver.quit()
Puppeteer — CDP on 9222
// npm install puppeteer-core
const puppeteer =
  require('puppeteer-core');

const browser = await puppeteer.connect({
  browserURL: 'http://127.0.0.1:9222',
});

// Spawns a "CDP" tab inside LumaBrowser.
const page = await browser.newPage();
await page.goto('https://example.com');

console.log(await page.title());

await browser.disconnect();
Playwright — connectOverCDP on 9222
// npm install playwright
const { chromium } = require('playwright');

const browser = await chromium.connectOverCDP(
  'http://127.0.0.1:9222',
);

// Default BrowserContext is pre-advertised,
// so .contexts()[0] resolves on connect.
const context = browser.contexts()[0]
  ?? await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');

console.log(await page.title());

await browser.close();

The Puppeteer and Playwright examples both land on the same WebSocket at ws://127.0.0.1:9222/devtools/browser/{uuid}. Same server, same targets, same Lumabyte.* domain — just two idiomatic client shapes.

Natural-language element resolution — side by side

The LumaByte differentiator. Vanilla Selenium, Puppeteer, and Playwright all ship without a deterministic-first LLM fallback for element resolution. All three drivers expose it here — only the dispatch syntax differs.

Selenium — capability + locator
opts = webdriver.ChromeOptions()
opts.set_capability(
  "lumabyte:llmFallback", { "enabled": True },
)
driver = webdriver.Remote(
  "http://127.0.0.1:9515", options=opts,
)

# Skip CSS entirely.
link = driver.find_element(
  "ai-description",
  "the more information link",
)

btn = driver.find_element(
  "ai-description",
  "the add to cart button",
)
btn.click()
Puppeteer — Lumabyte CDP domain
const cdp =
  await page.target().createCDPSession();

await cdp.send(
  'Lumabyte.configureFallback',
  { enabled: true },
);

// Tries selector first, LLM only on miss.
const r = await cdp.send('Lumabyte.find', {
  description: 'the more information link',
  selector:    'a.cta-deprecated',
});

await cdp.send('Lumabyte.click', {
  description: 'the add to cart button',
});
Playwright — Lumabyte via newCDPSession
const cdp =
  await context.newCDPSession(page);

await cdp.send(
  'Lumabyte.configureFallback',
  { enabled: true },
);

// Same domain, same pipeline, same server.
const r = await cdp.send('Lumabyte.find', {
  description: 'the more information link',
  selector:    'a.cta-deprecated',
});

await cdp.send('Lumabyte.click', {
  description: 'the add to cart button',
});
Which one should I pick?

Capabilities overlap heavily. The decision is about ergonomics and which ecosystem your stack already lives in.

Selenium if…

  • You already have a mature test suite in Selenium, WebdriverIO, Capybara, or Nightwatch. Change one URL and inherit LLM fallback.
  • You need cross-language clients: Python, Java, C#, Ruby, Kotlin, Go, PHP, Rust — WebDriver’s ecosystem is widest.
  • You route through Selenium Grid.
  • You want the automation to drive the user’s own tabs — useful for record/replay, visual QA of a real session.

Puppeteer if…

  • You’re in JavaScript/TypeScript agent-tooling land: browser-use, AgentCP, custom LLM orchestrators that already speak CDP.
  • You need native request interception (Fetch domain, page.setRequestInterception).
  • Your workflow listens for push events — network, console, DOM mutations, target lifecycle.
  • Stealth-sensitive scraping where navigator.webdriver is a tell.

Playwright if…

  • You want modern locators + auto-wait: getByRole, getByTestId, built-in expect assertions.
  • You’re building with @playwright/mcp — LumaBrowser is a drop-in --cdp-endpoint.
  • You need Playwright’s cross-language bindings (JS, Python, .NET, Java) with one client API.
  • You’re moving off bundled Chromium in CI — connectOverCDP kills the 170MB download.

Still unsure? All three are off by default. Enable them in LumaBrowser’s settings and try the same flow against each — they run concurrently without interfering with each other.

Enable any (or all)

1. Settings. Open Settings → Selenium or Settings → CDP Driver, check “Start automatically when LumaBrowser launches”, then click Save & Start.

2. REST.

# Start the Selenium WebDriver server (port 9515)
curl -X POST http://localhost:3000/api/selenium/start

# Start the CDP server (port 9222) — Puppeteer and Playwright both attach here
curl -X POST http://localhost:3000/api/cdp/start

# Check both
curl http://localhost:3000/api/selenium/status
curl http://localhost:3000/api/cdp/status

Full references:

FAQ

Can I run Selenium, Puppeteer, and Playwright against LumaBrowser at the same time?

Yes. Selenium binds to port 9515 on its own WebDriver server. Puppeteer and Playwright share a single CDP server on port 9222 — they attach with different client libraries but speak the same protocol. All three can drive LumaBrowser concurrently without collision: the CDP server creates its own automation tabs so the clients don’t fight each other or the user’s regular tabs for the webContents.debugger lock.

Do all three drivers share the LLM fallback?

Yes. Selenium, Puppeteer, and Playwright all route their natural-language-to-selector resolution through the same core selector-resolver slot and the same deterministic-match → LLM → validation pipeline. Improvements on any one path ship to all three at once.

Why do Puppeteer and Playwright share a server but Selenium has its own?

Because Puppeteer and Playwright (via connectOverCDP) both speak Chrome DevTools Protocol — the on-the-wire protocol is identical. Selenium speaks W3C WebDriver over HTTP, a completely different protocol. One CDP server handles both CDP-speaking clients; Selenium gets its own WebDriver server because there’s no protocol overlap.

Can I use Playwright’s native (non-CDP) protocol?

No — and this is deliberate. Playwright’s native protocol is proprietary, locked to client/driver minor versions, and changes with every Playwright release. connectOverCDP is Playwright’s documented path for attaching to an existing browser, it’s what every cloud-browser provider (Browserless, Browserbase, Steel.dev, Hyperbrowser) standardized on, and it’s versioned like CDP rather than like Playwright. The 80% of workflows that don’t rely on Firefox/WebKit, full tracing, or launch()-mode features work unchanged.

Which driver is faster?

For single commands, all three run in-process with the browser, so none has noticeable IPC overhead. The CDP drivers stream events instead of polling, which makes them faster for workloads that listen for network requests, console logs, or DOM mutations. Playwright via connectOverCDP adds one extra hop through its Node.js driver, but the difference only matters at very high CDP call volumes (thousands per page).

Is the LLM fallback opt-in everywhere?

Yes. In Selenium you set the lumabyte:llmFallback capability on new session. In Puppeteer and Playwright you call Lumabyte.configureFallback({ enabled: true }) on the CDP session. Either way, the LLM never runs unless you asked for it.

Why does the CDP driver use dedicated tabs?

Chromium’s webContents.debugger is an exclusive lock per tab. If CDP clients attached to the user’s regular tabs, LumaBrowser’s Network Interceptor, AI Chat browsing tools, and other subsystems would fight for the same handle. Dedicated cdp-kind tabs (rendered with a purple accent and CDP badge) sidestep the conflict and make the automation visible while it runs.

Ready to try it

Install LumaBrowser, enable any driver (or all), and point your existing client at the local port. The natural-language resolver is waiting.