Skip to main content
Selenium advanced Lesson 9 of 10

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.

Frequently Asked Questions

What is Selenium Grid?
A server that accepts WebDriver sessions and routes them to browsers running elsewhere. Your test code is unchanged apart from using `webdriver.Remote` with the Grid's URL, which is what lets one suite run across many machines and browser versions.
How many parallel sessions can a Grid node handle?
Roughly one browser per CPU core and about 1 GB of RAM each, so a 4-core 8 GB node comfortably runs four sessions. Oversubscribing makes every session slower and produces timing failures that look like application bugs.
Do I still need Grid if I can run tests locally?
Not for a small suite on one browser. Grid earns its place when you need browsers your machine cannot run, more parallelism than one machine provides, or a consistent browser version across a team and CI.
How do I debug a test running on a remote browser?
Use the Docker images that expose noVNC to watch the session live, enable video recording to review afterwards, and read the Grid's own session logs. Screenshots and page source from the failure hook work identically against a remote driver.