Selenium Grid and Remote WebDriver
Run the same suite against browsers on other machines — standalone and hub-node Grid in Docker, session capabilities, video recording, and sizing the pool.
Everything so far ran a browser on your machine. webdriver.Remote sends the same commands to
a Grid, which owns the browsers — the test code barely changes, and what you gain is browsers
you do not have and parallelism one laptop cannot provide.
Standalone Grid in one command
docker run -d --name grid -p 4444:4444 -p 7900:7900 --shm-size 2g \
selenium/standalone-chrome:4.28.1
Unable to find image 'selenium/standalone-chrome:4.28.1' locally
4.28.1: Pulling from selenium/standalone-chrome
Status: Downloaded newer image for selenium/standalone-chrome:4.28.1
7f3c9a1b2d4e5f60718293a4b5c6d7e8f901234567890abcdef1234567890abcd
curl -s http://localhost:4444/status | python -m json.tool
{
"value": {
"ready": true,
"message": "Selenium Grid ready.",
"nodes": [
{
"id": "8f2c1a44-8e21-4b0e-9a3c-1d84f0b27a51",
"maxSessions": 1,
"availability": "UP",
"slots": [
{
"stereotype": {"browserName": "chrome", "browserVersion": "133.0"}
}
]
}
]
}
}
--shm-size 2g is not optional. Chrome uses shared memory heavily, and Docker’s 64 MB default
produces crashes that surface as session deleted because of page crash — the single most
common Grid-in-Docker problem.
Pointing tests at it
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--lang=en-GB")
options.set_capability("se:name", "adds a todo")
driver = webdriver.Remote(command_executor="http://localhost:4444", options=options)
try:
driver.get("https://demo.playwright.dev/todomvc")
print("title: ", driver.title)
print("session:", driver.session_id)
caps = driver.capabilities
print("browser:", caps["browserName"], caps["browserVersion"])
finally:
driver.quit()
title: React • TodoMVC
session: 3d9c4b21771a4e02b8f15c2e91a4d883
browser: chrome 133.0.6943.16
That is the whole change: webdriver.Remote(command_executor=...) instead of
webdriver.Chrome(). Every locator, wait and page object from earlier lessons works
untouched.
Fold it into the fixture from lesson 8:
# conftest.py
def pytest_addoption(parser):
parser.addoption("--grid", default=None, help="Grid URL, e.g. http://localhost:4444")
@pytest.fixture
def driver(request):
browser = request.config.getoption("--browser")
grid = request.config.getoption("--grid")
options = {"chrome": webdriver.ChromeOptions,
"firefox": webdriver.FirefoxOptions,
"edge": webdriver.EdgeOptions}[browser]()
if not request.config.getoption("--headed"):
options.add_argument("-headless" if browser == "firefox" else "--headless=new")
options.set_capability("se:name", request.node.name)
drv = (webdriver.Remote(command_executor=grid, options=options) if grid
else _make_local_driver(browser, options))
drv.implicitly_wait(0)
yield drv
drv.quit()
pytest -n 4 --grid http://localhost:4444 -v
4 workers [24 items]
........................ [100%]
========================= 24 passed in 48.11s =========================
Same suite, same command, browsers running in a container. se:name puts the test name in the
Grid UI, which is how you tell twelve concurrent sessions apart.
Watching a session
The standalone images expose noVNC on port 7900:
open http://localhost:7900 # password: secret
Connected to Selenium standalone-chrome (noVNC)
You can watch a headed session live — invaluable for a failure that only reproduces on the Grid. Add video recording for the failures you were not watching:
options.set_capability("se:recordVideo", True)
options.set_capability("se:screenResolution", "1440x900")
options.set_capability("se:timeZone", "Europe/London")
docker run -d --name grid -p 4444:4444 --shm-size 2g \
-v /tmp/videos:/videos -e SE_VIDEO_FILE_NAME=auto \
selenium/standalone-chrome:4.28.1
ls /tmp/videos
adds_a_todo_chrome_133.0_20260910073412.mp4
completes_a_todo_chrome_133.0_20260910073419.mp4
se:timeZone and se:screenResolution are worth setting explicitly — they are the
environment variables from lesson 7, pinned at the Grid rather than in browser options.
Hub and nodes
Standalone is one machine. For a real pool, run a hub and attach nodes:
# docker-compose.yml
services:
hub:
image: selenium/hub:4.28.1
ports: ["4442:4442", "4443:4443", "4444:4444"]
environment:
- SE_SESSION_REQUEST_TIMEOUT=300
- SE_SESSION_RETRY_INTERVAL=2
chrome:
image: selenium/node-chrome:4.28.1
shm_size: 2gb
depends_on: [hub]
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=4
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
- SE_NODE_SESSION_TIMEOUT=120
deploy:
replicas: 3
firefox:
image: selenium/node-firefox:4.28.1
shm_size: 2gb
depends_on: [hub]
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=2
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
docker compose up -d --scale chrome=3
curl -s http://localhost:4444/status | python -c "
import json,sys
d = json.load(sys.stdin)['value']
print('ready:', d['ready'])
for n in d['nodes']:
browsers = {s['stereotype']['browserName'] for s in n['slots']}
print(f\" node {n['id'][:8]} {sorted(browsers)} max={n['maxSessions']} {n['availability']}\")
"
ready: True
node 8f2c1a44 ['chrome'] max=4 UP
node 3d9c4b21 ['chrome'] max=4 UP
node a1b2c3d4 ['chrome'] max=4 UP
node 77a14e02 ['firefox'] max=2 UP
14 slots. Requests are queued by the hub and matched to a node whose stereotype fits, so a test asking for Firefox waits for a Firefox slot rather than failing.
pytest -n 12 --grid http://localhost:4444 --browser chrome
12 workers [96 items]
................................................................................ [ 83%]
................ [100%]
========================= 96 passed in 142.08s ========================
Sizing. Roughly one browser per core and about 1 GB of RAM each. SE_NODE_MAX_SESSIONS=4
on a 4-core node is right; setting it to 12 to “go faster” makes every session slower and
produces timeouts that look like application bugs. SE_NODE_OVERRIDE_MAX_SESSIONS=true is
required to exceed the CPU-count default, which is itself a hint that you usually should not.
Ask for more sessions than exist and the hub queues you:
selenium.common.exceptions.SessionNotCreatedException: Message: Could not start a new
session. Could not start a new session. New session request timed out
Host info: host: 'runner-01', ip: '10.4.2.19'
SE_SESSION_REQUEST_TIMEOUT controls how long a request waits for a slot. Raise it when a
burst of workers exceeds the pool briefly; if it times out routinely, the pool is too small.
Targeting a specific browser version
options = webdriver.ChromeOptions()
options.browser_version = "132.0"
options.platform_name = "linux"
driver = webdriver.Remote("http://localhost:4444", options=options)
print(driver.capabilities["browserVersion"])
132.0.6834.83
If no node matches, the failure is explicit rather than silently running the wrong version:
selenium.common.exceptions.SessionNotCreatedException: Message: Could not start a new
session. Response code 500. Message: Unable to find provider for session:
Capabilities {browserName: chrome, browserVersion: 118.0, platformName: linux}
Reproducing a version-specific bug is one of the strongest reasons to run a Grid at all — pin the version, reproduce, then move on.
Timeouts that matter
- SE_NODE_SESSION_TIMEOUT=120 # kill a session idle this long
- SE_SESSION_REQUEST_TIMEOUT=300 # how long a client waits for a slot
driver.set_page_load_timeout(30)
driver.set_script_timeout(30)
A test that crashes without quit() leaves a session holding a slot. SE_NODE_SESSION_TIMEOUT
reclaims it — without that, a handful of leaked sessions can starve the pool and every
subsequent run “hangs” for no visible reason. The fixture teardown from lesson 1 is the real
fix; this is the safety net.
A CI job against Grid
# .github/workflows/e2e-grid.yml
jobs:
test:
runs-on: ubuntu-latest
services:
selenium:
image: selenium/standalone-chrome:4.28.1
ports: ["4444:4444"]
options: --shm-size=2g
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- run: pip install -r requirements.txt
- name: Wait for Grid
run: |
for i in $(seq 1 30); do
curl -sf http://localhost:4444/status | grep -q '"ready": true' && exit 0
sleep 2
done
exit 1
- run: pytest -n 4 --grid http://localhost:4444 --junitxml=results.xml
Wait for Grid
✓ Grid ready after 6s
Run pytest -n 4 --grid http://localhost:4444
4 workers [24 items]
........................ [100%]
========================= 24 passed in 52.44s =========================
The readiness loop matters: a service container accepting TCP connections is not the same as a
Grid with registered nodes, and starting tests too early produces SessionNotCreatedException
on the first few.
Cloud grids
The same code points at a hosted provider:
options = webdriver.ChromeOptions()
options.browser_version = "133.0"
options.platform_name = "Windows 11"
options.set_capability("cloud:options", {
"build": os.environ["GIT_SHA"],
"name": "checkout flow",
"video": True,
})
driver = webdriver.Remote(
command_executor=f"https://{USER}:{KEY}@hub.provider.example/wd/hub",
options=options,
)
session: a1b2c3d4e5f60718293a4b5c6d7e8f90
browser: chrome 133.0 on Windows 11
Worth it for real Safari, real mobile browsers, and Windows-specific rendering — none of which a Linux Docker Grid can give you. The trade is per-minute cost and latency: every WebDriver command is now a round trip over the internet, so a chatty test that runs in 4 seconds locally can take 40.
Practice
1. Start a standalone Grid and run the suite against it.
4 workers [24 items]
========================= 24 passed in 48.11s =========================
The only change is webdriver.Remote(command_executor=...). Everything from the earlier
lessons — locators, waits, page objects — is untouched.
2. Omit --shm-size and run a heavy page.
selenium.common.exceptions.WebDriverException: Message: unknown error: session deleted
because of page crash
from unknown error: cannot determine loading status
from tab crashed
Docker’s 64 MB /dev/shm default. It presents as a random crash rather than a resource error,
which is why it wastes so much time the first time you meet it.
3. Request more parallel sessions than the Grid has slots.
selenium.common.exceptions.SessionNotCreatedException: Message: Could not start a new
session. New session request timed out
The hub queued the request and gave up. Either scale nodes or lower -n — running 12 workers
against 4 slots is slower than running 4, because of the retry churn.
4. Pin a browser version the Grid does not have.
Unable to find provider for session: Capabilities {browserName: chrome,
browserVersion: 118.0, platformName: linux}
An explicit failure rather than a silent fallback to whatever is available. That guarantee is what makes a Grid useful for reproducing version-specific bugs.
Next: Selenium 4’s newer capabilities — CDP, BiDi, and what they replace.