How to Migrate from Selenium Grid to BrowserQL
A practical migration plan from Selenium Grid to BrowserQL, including protocol mapping, state, assertions, pilots, troubleshooting, and code.
Short answer: migrating from Selenium Grid to BrowserQL is a rewrite of the browser-control layer. BrowserQL is a GraphQL protocol: your client sends mutations describing navigation, interaction, extraction, waits, screenshots, and related work, then receives structured data. It is not a Selenium-compatible endpoint and does not accept existing WebDriver commands.
The safest migration is a pilot on one representative end-to-end flow. Inventory the Grid suite, select a flow that exercises the state and browser features you depend on, translate each WebDriver action into a BrowserQL mutation, keep your existing test runner and reporting where useful, and compare the result with the Grid implementation before migrating more coverage.
What changes when you leave Selenium Grid
Selenium Grid distributes WebDriver sessions across hubs, routers, and browser nodes. Your test code creates a RemoteWebDriver, sends imperative calls such as get(), findElement(), sendKeys(), and click(), and reads browser state through WebDriver objects.
BrowserQL is a GraphQL API for browser automation. A request contains mutations or queries, and the response contains structured fields that your test code can inspect. Browserless documents BrowserQL as a separate protocol from its managed-browser BaaS offering. Browserless BaaS v2 speaks Chrome DevTools Protocol rather than WebDriver, so neither option is a drop-in Selenium Grid URL.
Read the vendor’s migration guide, the BrowserQL documentation, and the BaaS v2 migration guide while checking the current endpoint and plan limits.
| Concern | Selenium Grid | BrowserQL |
|---|---|---|
| Control model | Imperative WebDriver calls on a remote browser session | GraphQL mutations and queries with structured responses |
| Infrastructure | You operate or provision Grid components, drivers, nodes, and browser capabilities | Browser execution is exposed through the BrowserQL service |
| Code reuse | Existing WebDriver code | Translate browser actions and assertions to BQL; retain surrounding test code where practical |
| State | Usually held by a WebDriver session object | Requests can be independent; use reconnect/session mechanisms when cookies, cache, or page state must persist |
| Result handling | DOM and browser objects | JSON fields returned by the GraphQL operation |
| Alternative path | Keep Selenium if its protocol and infrastructure remain the best fit | Use Browserless BaaS when retaining Puppeteer or Playwright code is more important than adopting BQL |
Plan the migration before changing code
1. Inventory the current Grid suite
Record the assumptions hidden in your test code and Grid configuration:
- Programming languages, test runners, assertion libraries, and reporting integrations.
- Every WebDriver operation: navigation, waits, clicks, typing, JavaScript execution, screenshots, downloads, frames, windows, and extraction.
- Browser and operating-system capabilities, versions, viewport sizes, locale, timezone, proxy, user agent, and permissions.
- Driver installation and custom startup options.
- Parallelism, queueing, retry behavior, and maximum test duration.
- Authentication, cookies, local storage, cross-test state, and whether a sequence must stay in one browser.
- Sites protected by bot checks or CAPTCHA challenges.
- Assertions that depend on WebDriver object types rather than observable page results.
This inventory is an editorial migration checklist, not an automated BrowserQL tool. Use it to identify the smallest flow that still represents the difficult parts of your suite.
2. Choose one representative pilot
Pick one end-to-end test rather than a trivial page load. A useful pilot normally includes the interactions and state that make your current Grid setup valuable: for example, opening a login page, entering credentials, submitting a form, waiting for a result, and asserting a value. Avoid starting with the most unusual workflow or a flow that depends on an unsupported browser feature.
Run the pilot beside the existing Grid test. Compare coverage, observable behavior, assertion results, runtime, failure modes, state handling, concurrency, and operational work. The Browserless sources provide migration guidance, not an independent benchmark, so measure your own application.
Map WebDriver actions to BrowserQL
Translate the intent of each action instead of trying to reproduce the WebDriver object model. The exact field names available depend on the current BrowserQL schema; confirm them in the schema documentation or BQL editor.
| WebDriver intent | BrowserQL shape | Migration note |
|---|---|---|
| Open a URL | goto(url: ...) |
Assert the returned status or another response field. |
| Wait for a page condition | Use documented wait options or a wait mutation | Replace arbitrary sleeps with a condition tied to the page. |
| Click an element | click(selector: ...) |
Use a stable selector and inspect the mutation result. |
| Type into a field | type(selector: ..., text: ...) |
Keep secrets out of logs and source control. |
| Read text or an attribute | Use the documented extraction query or mutation | Assert on returned structured data, not a WebElement. |
| Take a screenshot or PDF | Use the documented screenshot/PDF operation | Store the returned data or URL according to your workflow. |
| Reuse cookies and page state | reconnect or a supported session mechanism |
Design timeout, ownership, and cleanup explicitly. |
Run a first BrowserQL request
Set BQL_ENDPOINT to the BrowserQL endpoint shown in your Browserless account and BROWSERLESS_TOKEN to the appropriate token. The mutation below demonstrates the basic request model: navigate, interact, and return structured fields.
cURL
export BQL_ENDPOINT='https://YOUR-BROWSERQL-ENDPOINT'
export BROWSERLESS_TOKEN='YOUR_TOKEN'
curl -sS "$BQL_ENDPOINT?token=$BROWSERLESS_TOKEN" \
-H 'content-type: application/json' \
--data-binary @- <<'JSON'
{
"query": "mutation LoginFlow($url: String!, $email: String!, $password: String!) { goto(url: $url) { status } type(selector: \"input[type=email]\", text: $email) { ... on TypeResponse { time } } type(selector: \"input[type=password]\", text: $password) { ... on TypeResponse { time } } click(selector: \"button[type=submit]\") { ... on ClickResponse { time } } }",
"variables": {
"url": "https://example.com/login",
"email": "user@example.com",
"password": "replace-me"
}
}
JSON
Schema response types can change. If the editor reports that a field or inline fragment is invalid, inspect the current schema and select the fields exposed by your account.
Python
import os
import requests
endpoint = os.environ["BQL_ENDPOINT"]
token = os.environ["BROWSERLESS_TOKEN"]
query = """
mutation LoginFlow($url: String!, $email: String!, $password: String!) {
goto(url: $url) { status }
type(selector: "input[type=email]", text: $email) { ... on TypeResponse { time } }
type(selector: "input[type=password]", text: $password) { ... on TypeResponse { time } }
click(selector: "button[type=submit]") { ... on ClickResponse { time } }
}
"""
response = requests.post(
endpoint,
params={"token": token},
json={
"query": query,
"variables": {
"url": "https://example.com/login",
"email": "user@example.com",
"password": "replace-me",
},
},
timeout=90,
)
response.raise_for_status()
payload = response.json()
if payload.get("errors"):
raise RuntimeError(payload["errors"])
print(payload["data"])
Node.js
const endpoint = process.env.BQL_ENDPOINT;
const token = process.env.BROWSERLESS_TOKEN;
const query = `
mutation LoginFlow($url: String!, $email: String!, $password: String!) {
goto(url: $url) { status }
type(selector: "input[type=email]", text: $email) { ... on TypeResponse { time } }
type(selector: "input[type=password]", text: $password) { ... on TypeResponse { time } }
click(selector: "button[type=submit]") { ... on ClickResponse { time } }
}
`;
const res = await fetch(`${endpoint}?token=${encodeURIComponent(token)}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
query,
variables: {
url: 'https://example.com/login',
email: 'user@example.com',
password: 'replace-me'
}
})
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const payload = await res.json();
if (payload.errors) throw new Error(JSON.stringify(payload.errors));
console.log(payload.data);
Keep assertions in your existing test framework
You can call BrowserQL over HTTP or WebSocket from JUnit, pytest, Mocha, and other runners. Keep fixtures, test discovery, reports, and CI integration where they still add value. Rewrite the portions coupled to WebDriver, WebElement, and driver lifecycle.
# Pseudocode inside an existing test
payload = run_browserql(login_mutation, variables)
assert payload["data"]["goto"]["status"] == 200
assert payload["data"]["click"] is not None
Prefer assertions on stable, business-relevant outputs. If the mutation returns extracted text, a URL, a status, or a count, assert that structured value. Avoid making every internal timing field part of the contract.
Design state and session lifetime deliberately
Independent BQL requests are useful when every operation can establish its own context. A login flow, checkout sequence, or multi-step extraction often needs continuity. Browserless documents a reconnect mutation that returns a BrowserQL endpoint for the same running browser. Cookies, cache, and page state can survive across requests.
mutation StartAndReconnect {
goto(url: "https://example.com") {
status
}
reconnect(timeout: 60000) {
browserQLEndpoint
browserWSEndpoint
}
}
The timeout value is the idle period, in milliseconds, before the disconnected browser is terminated. Reconnecting resets the idle timer, but the session still has an absolute duration limit determined by the plan. Close sessions promptly when the flow ends and handle a lost endpoint as a recoverable session failure, not as proof that the page is broken. See the reconnect documentation for the current behavior.
State checklist
- Decide whether each test starts from a clean browser.
- Never assume a cookie or local-storage value exists unless the test created it or the session contract supplies it.
- Give each parallel flow its own session identity.
- Set an idle timeout longer than the expected gap between requests but short enough to release capacity.
- Set an overall test deadline and stop reconnecting after that deadline.
- Terminate or allow the session to expire after success, failure, and cancellation.
- Do not log tokens, passwords, cookies, or full GraphQL variables.
Handle waits, dynamic pages, and flaky interactions
Grid suites often hide timing assumptions in implicit waits, explicit waits, polling loops, and sleeps. During translation, classify each wait:
- Navigation wait: wait for the documented page-load condition when a URL transition is the dependency.
- Element wait: wait for a selector to exist or become actionable before clicking or typing.
- Application wait: wait for a business result such as a rendered table, confirmation text, or changed URL.
- Network wait: use a documented network-idle option only when network quiescence is actually the correct signal.
Replace fixed sleeps with the narrowest condition that proves the next action is safe. If a site uses animations, virtualized lists, or delayed hydration, capture diagnostics for the failing state and then choose a selector or application condition that remains stable across runs.
Choose BrowserQL or managed BaaS
BrowserQL and Browserless BaaS solve different migration problems:
| Choose BrowserQL when… | Choose BaaS when… |
|---|---|
| You want a declarative GraphQL interface and structured responses. | You want to keep a substantial Puppeteer or Playwright codebase. |
| You are willing to translate WebDriver actions and assertions. | Your team’s main requirement is a managed browser controlled through an existing CDP-based library. |
| Independent mutations or explicitly designed reconnect sessions fit the workflow. | The selected library’s session and page abstractions map directly to your application. |
Neither route makes Selenium/WebDriver compatible with Browserless BaaS v2. If preserving Selenium code is the overriding requirement, keep evaluating a Selenium-compatible service separately rather than treating BrowserQL as a replacement endpoint.
Run the pilot beside Grid
- Freeze a representative Grid test and record its inputs, capabilities, expected assertions, and cleanup behavior.
- Translate navigation, waits, interaction, extraction, and screenshots into BQL operations.
- Keep the same test data and assertion intent.
- Run both implementations against the same environment and record pass/fail outcomes and failure classifications.
- Repeat enough times to expose intermittent failures; do not call the result a benchmark.
- Exercise parallel sessions, authentication renewal, cancellation, and cleanup.
- Check browser features the suite requires, including frames, downloads, popups, permissions, and CAPTCHA-related workflows.
- Decide whether to migrate more tests, keep a hybrid suite, or evaluate BaaS for workflows better served by Puppeteer or Playwright.
Performance, reliability, and cost considerations
Performance
- Measure end-to-end test time, including request latency, browser startup, navigation, waits, and teardown.
- Do not assume that fewer lines of BQL means a faster test.
- Reconnect when repeated requests would otherwise reload the same page, while respecting idle and absolute session limits.
- Use parallelism only after defining session isolation and service concurrency limits.
Reliability
- Classify failures as transport errors, GraphQL errors, browser navigation failures, selector failures, application failures, session expiry, or assertion failures.
- Retry only operations that are safe to repeat. A blind retry of a payment or form submission can duplicate side effects.
- Capture the returned structured error, operation name, correlation data available to your account, and a redacted request summary.
- Close sessions on every exit path and use bounded timeouts.
Cost
Compare the total operating cost of Grid infrastructure, driver and browser maintenance, CI capacity, and engineering time with the Browserless plan and concurrency limits that apply to your account. The dossier does not establish an independent cost or speed advantage for every application. Use your pilot measurements and current provider terms.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| HTTP authentication or authorization failure | Wrong endpoint, missing token, or token passed in the wrong place | Copy the current endpoint and authentication format from the Browserless documentation; rotate exposed tokens. |
| GraphQL validation error | Mutation, argument, field, or response type does not match the current schema | Run the operation in the BQL editor and inspect the live schema; update fields and inline fragments. |
| Navigation succeeds but the next action fails | Hydration, redirect, animation, or selector timing | Use a condition-based wait and a stable selector; avoid increasing fixed sleeps blindly. |
| State disappears between requests | Requests created independent browsers | Use reconnect/session handling and pass the returned endpoint to the next request. |
| Reconnect endpoint expires | Idle timeout or absolute session duration was exceeded | Reconnect sooner, increase the allowed idle timeout where supported, or split the workflow into bounded phases. |
| Parallel tests affect one another | Cookies, storage, or reconnect endpoints were shared | Allocate one isolated session per flow and remove shared mutable state. |
| Assertions are brittle | Assertions depend on WebDriver objects or transient DOM details | Assert on structured BQL results and stable business-level values. |
| CAPTCHA or bot check blocks the flow | The target requires capabilities or handling not present in the translated flow | Confirm the documented BrowserQL capabilities and test the exact target flow; do not claim compatibility without a pilot. |
| Tests leak browsers | No cleanup on failure or cancellation | Put session cleanup in a finally/teardown path and enforce an outer deadline. |
Or skip the browser setup
If your goal is a clean screenshot or PDF rather than a full interactive browser test, ScreenshotNeo provides a single GET request. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, PDF paper size and page ranges, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, custom headers and cookies, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, usage reporting, and an OpenAPI specification. Every feature is on every plan. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and start with 1,000 screenshots a month without adding a card.
FAQ
Is BrowserQL a Selenium Grid replacement endpoint?
No. It is a GraphQL browser-automation protocol. Existing WebDriver commands must be translated into BrowserQL operations.
Can I keep my current test framework?
Usually, yes. Call BrowserQL over HTTP or WebSocket from the existing runner and retain reporting and assertion organization where practical. Replace code coupled to WebDriver sessions and elements.
Should every request reconnect to the same browser?
No. Use independent requests for independent work. Use reconnect or a supported session mechanism only when cookies, cache, or page state must persist.
What if the team mainly uses Playwright?
Evaluate Browserless BaaS as a separate option because it is positioned for managed browsers controlled through Puppeteer or Playwright. That does not add Selenium/WebDriver support.
How do I prove the migration is ready?
Run a representative flow beside Grid, compare behavior and failure classes under realistic parallelism, and verify state, cleanup, browser features, and operational limits before migrating additional tests.


