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.
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.
| 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. |
Same browser, same LumaBrowser instance, three different handshakes:
# 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()// 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();// 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.
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.
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()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',
});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',
});Capabilities overlap heavily. The decision is about ergonomics and which ecosystem your stack already lives in.
browser-use, AgentCP, custom LLM orchestrators that already speak CDP.Fetch domain, page.setRequestInterception).navigator.webdriver is a tell.getByRole, getByTestId, built-in expect assertions.@playwright/mcp — LumaBrowser is a drop-in --cdp-endpoint.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.
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/statusFull references:
connectOverCDP + Playwright MCPYes. 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.
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.
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.
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.
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).
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.
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.
Install LumaBrowser, enable any driver (or all), and point your existing client at the local port. The natural-language resolver is waiting.