Distributed Testing with Selenium Grid and Zalenium
Running Selenium tests across browsers means running on multiple machines. A single browser session is single-threaded — you cannot run Chrome and Firefox tests simultaneously on one WebDriver instance. Selenium Grid solves this by acting as a router: your tests send WebDriver requests to the Grid, and the Grid dispatches them to machines that have the right browser available.
This guide covers Selenium Grid 4's architecture, how to set it up with Docker Compose, how Zalenium extends Grid with dynamic scaling and video recording, and how to deploy Grid on Kubernetes for production-scale testing.
Selenium Grid 4 Architecture
Grid 4 was rewritten from scratch compared to Grid 3. The architecture has four distinct components:
Router — The entry point for all WebDriver requests. The Router forwards new session requests to the Distributor and routes ongoing session requests to the correct Node.
Distributor — Maintains a registry of all Nodes and their available slots. When a new session request arrives, the Distributor finds a Node with a matching browser capability and creates the session there.
Session Map — Tracks which session is running on which Node. Required for the Router to forward subsequent requests (mouse clicks, navigation, etc.) to the right machine.
Node — The machine that actually runs the browser. A Node registers with the Distributor, advertises its capabilities (browsers, versions, platform), and accepts WebDriver sessions.
In the simplest deployment, all four components run in the same process:
# Standalone mode — all components in one process
java -jar selenium-server.jar standalone
# Default: listens on port 4444
# Max sessions: defaults to CPU countFor distributed deployment, each component runs separately:
# Start the Event Bus (required for distributed mode)
java -jar selenium-server.jar event-bus \
--publish-events tcp://0.0.0.0:4442 \
--subscribe-events tcp://0.0.0.0:4443
# Start the Session Map
java -jar selenium-server.jar sessions \
--publish-events tcp://event-bus:4442 \
--subscribe-events tcp://event-bus:4443
# Start the Distributor
java -jar selenium-server.jar distributor \
--publish-events tcp://event-bus:4442 \
--subscribe-events tcp://event-bus:4443 \
--sessions http://session-map:5556
# Start the Router (Hub equivalent)
java -jar selenium-server.jar router \
--sessions http://session-map:5556 \
--distributor http://distributor:5553 \
--session-queue http://session-queue:5559
# Start Nodes (one per machine/container)
java -jar selenium-server.jar node \
--publish-events tcp://event-bus:4442 \
--subscribe-events tcp://event-bus:4443For most teams, the Hub mode simplifies this — it runs Router, Distributor, Session Map, and Session Queue together:
# Hub mode (Router + Distributor + Session Map + Queue)
java -jar selenium-server.jar hub
# Node registers with Hub
java -jar selenium-server.jar node \
--hub http://hub:4444Docker Compose Setup
The easiest way to run Grid locally or in CI is with Docker Compose. The Selenium project provides official images for every component:
# docker-compose.yml
version: '3.8'
services:
chrome:
image: selenium/node-chrome:4.21.0-20240522
shm_size: '2gb' # Required: Chrome uses /dev/shm for rendering
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=4
- SE_NODE_OVERRIDE_MAX_SESSIONS=true
deploy:
replicas: 3 # 3 Chrome nodes, 4 sessions each = 12 concurrent Chrome sessions
firefox:
image: selenium/node-firefox:4.21.0-20240522
shm_size: '2gb'
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=2
deploy:
replicas: 2 # 2 Firefox nodes, 2 sessions each = 4 concurrent Firefox sessions
edge:
image: selenium/node-edge:4.21.0-20240522
shm_size: '2gb'
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=2
selenium-hub:
image: selenium/hub:4.21.0-20240522
ports:
- '4444:4444'
- '4442:4442'
- '4443:4443'
environment:
- SE_SESSION_REQUEST_TIMEOUT=300
- SE_SESSION_RETRY_INTERVAL=5
- GRID_MAX_SESSION=20Start the Grid:
docker compose up -d
# Check Grid status
curl http://localhost:4444/status | jq '.value.ready'
# true
# View registered nodes and sessions
curl http://localhost:4444/status | jq '.value.nodes'The Grid UI is available at http://localhost:4444/ui and shows live session status, Node health, and pending requests.
Configuring WebDriver to Use Grid
Change the WebDriver URL from a local driver to the Grid endpoint:
# Python — before (local)
from selenium import webdriver
driver = webdriver.Chrome()
# Python — after (Grid)
from selenium import webdriver
from selenium.webdriver.common.options import ArgOptions
options = webdriver.ChromeOptions()
driver = webdriver.Remote(
command_executor='http://selenium-hub:4444/wd/hub',
options=options,
)// JavaScript (WebdriverIO)
// wdio.conf.js — before (local)
exports.config = {
services: ['chromedriver'],
capabilities: [{ browserName: 'chrome' }],
};
// wdio.conf.js — after (Grid)
exports.config = {
hostname: 'selenium-hub',
port: 4444,
path: '/wd/hub',
capabilities: [
{ browserName: 'chrome' },
{ browserName: 'firefox' }, // Grid routes to the right node
],
};// Java — connecting to Grid
ChromeOptions options = new ChromeOptions();
options.setPlatformName("linux");
options.setBrowserVersion("latest");
WebDriver driver = new RemoteWebDriver(
new URL("http://selenium-hub:4444/wd/hub"),
options
);Running Tests in Parallel Across Browsers
The power of Grid is running multiple browsers simultaneously. With TestNG (Java):
// testng.xml — parallel across browsers
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="CrossBrowserSuite" parallel="tests" thread-count="3">
<test name="Chrome Tests">
<parameter name="browser" value="chrome"/>
<classes>
<class name="com.example.tests.LoginTest"/>
<class name="com.example.tests.CheckoutTest"/>
</classes>
</test>
<test name="Firefox Tests">
<parameter name="browser" value="firefox"/>
<classes>
<class name="com.example.tests.LoginTest"/>
<class name="com.example.tests.CheckoutTest"/>
</classes>
</test>
<test name="Edge Tests">
<parameter name="browser" value="edge"/>
<classes>
<class name="com.example.tests.LoginTest"/>
<class name="com.example.tests.CheckoutTest"/>
</classes>
</test>
</suite>// BaseTest.java
public class BaseTest {
protected WebDriver driver;
@BeforeMethod
@Parameters("browser")
public void setup(String browser) throws MalformedURLException {
MutableCapabilities options = switch (browser) {
case "chrome" -> new ChromeOptions();
case "firefox" -> new FirefoxOptions();
case "edge" -> new EdgeOptions();
default -> throw new IllegalArgumentException("Unknown browser: " + browser);
};
driver = new RemoteWebDriver(
new URL("http://selenium-hub:4444/wd/hub"),
options
);
}
@AfterMethod
public void teardown() {
if (driver != null) driver.quit();
}
}Zalenium: Dynamic Grid with Video Recording
Zalenium (from Zalando, now community-maintained) adds several features on top of Selenium Grid:
- Dynamic node scaling: Nodes spin up on demand and shut down when idle
- Video recording: Every session recorded automatically
- Live preview: Watch tests run in real time via VNC
- Test result dashboard: Pass/fail history with video links
- Integration: Sends results to TestingBot, Sauce Labs, BrowserStack as fallback
# docker-compose.yml with Zalenium
version: '3.8'
services:
zalenium:
image: dosel/zalenium:3
ports:
- '4444:4444'
- '5555:5555' # VNC port for live preview
volumes:
- /tmp/videos:/home/seluser/videos
- /var/run/docker.sock:/var/run/docker.sock # Zalenium manages Docker containers
environment:
- PULL_SELENIUM_IMAGE=true
privileged: true # Required for Docker-in-Docker
command: start --desiredContainers 2 --maxDockerSeleniumContainers 8Key Zalenium environment variables:
# docker run flags for Zalenium
--desiredContainers 2 # Start with 2 nodes ready
--maxDockerSeleniumContainers 8 # Scale up to 8 nodes under load
--screenWidth 1920 # Browser viewport width
--screenHeight 1080 # Browser viewport height
--timeZone Europe/Berlin # Timezone in containers
--videoRecordingEnabled true
--keepVideoFolder true # Keep videos after session endsZalenium's dashboard at http://localhost:4444/grid/admin/live shows active sessions with live video. Test results at http://localhost:4444/dashboard show pass/fail status with links to recordings.
Sending Test Results to Zalenium
Zalenium picks up test status from WebDriver capabilities:
# Python — mark test as passed or failed
options = webdriver.ChromeOptions()
options.set_capability('name', 'Login Test - Chrome')
driver = webdriver.Remote(
command_executor='http://localhost:4444/wd/hub',
options=options,
)
try:
# ... test code ...
driver.execute_script("sauce:job-result=passed") # Zalenium uses same syntax
except Exception as e:
driver.execute_script("sauce:job-result=failed")
raise
finally:
driver.quit()Monitoring Grid Utilization
Grid 4 exposes a GraphQL endpoint for monitoring:
# Query Grid status via GraphQL
curl -X POST http://localhost:4444/graphql \
-H 'Content-Type: application/json' \
-d '{
"query": "{
grid {
maxSession
nodeCount
sessionCount
totalSlots
usedSlots
}
nodesInfo {
nodes {
id
status
sessionCount
maxSession
browsers {
browserName
browserVersion
}
}
}
}"
}' | jq '.data'Integrate this into a Grafana dashboard:
# prometheus-selenium-grid-exporter
# Use selenium-grid-exporter to scrape Grid metrics for Prometheus
version: '3.8'
services:
grid-exporter:
image: viniciusccarvalho/selenium-grid-exporter:latest
environment:
- GRID_URL=http://selenium-hub:4444
ports:
- '8080:8080' # Prometheus scrapes /metrics hereKey metrics to track:
selenium_grid_session_active— current active sessionsselenium_grid_session_queue_size— sessions waiting for a nodeselenium_grid_node_count— total registered nodes- Session duration p50/p95 — catch tests that run too long
Kubernetes Deployment
For production scale, deploy Grid on Kubernetes using the official Helm chart:
# Add the Selenium Helm repo
helm repo add docker-selenium https://www.selenium.dev/docker-selenium-chart
helm repo update
# Install Grid with autoscaling
helm install selenium-grid docker-selenium/selenium-grid \
--set autoscaling.enabled=true \
--set chromeNode.replicas=2 \
--set firefoxNode.replicas=1 \
--set autoscaling.scalingType=job \
--namespace selenium \
--create-namespaceThe Helm chart configures KEDA (Kubernetes Event Driven Autoscaling) to scale nodes based on the Grid's session queue depth. When tests are queued, new Node pods spin up. When the queue empties, pods scale back to the minimum.
Custom values file for fine-grained control:
# selenium-values.yaml
hub:
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
chromeNode:
replicas: 2
maxReplicaCount: 10
resources:
requests:
memory: "1Gi"
cpu: "1000m"
limits:
memory: "2Gi"
cpu: "2000m"
extraEnvironmentVariables:
- name: SE_NODE_MAX_SESSIONS
value: "2"
- name: SE_VNC_NO_PASSWORD
value: "1"
autoscaling:
enabled: true
scalingType: job
scaledOptions:
minReplicaCount: 1
maxReplicaCount: 10
cooldownPeriod: 300
triggers:
- type: selenium-grid
metadata:
url: "http://selenium-hub:4444"
browserName: "chrome"
sessionBrowserCapability: "true"
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: nginx
hostname: selenium-grid.internal.example.comhelm install selenium-grid docker-selenium/selenium-grid \
-f selenium-values.yaml \
--namespace seleniumCI Integration with Grid
In GitHub Actions, spin up Grid as a service container:
jobs:
selenium-tests:
runs-on: ubuntu-latest
services:
selenium-hub:
image: selenium/hub:4.21.0-20240522
ports:
- 4444:4444
- 4442:4442
- 4443:4443
chrome:
image: selenium/node-chrome:4.21.0-20240522
options: --shm-size=2g
env:
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: 4442
SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
SE_NODE_MAX_SESSIONS: 4
firefox:
image: selenium/node-firefox:4.21.0-20240522
options: --shm-size=2g
env:
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: 4442
SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Wait for Grid
run: |
until curl -s http://localhost:4444/status | jq -e '.value.ready'; do
echo "Waiting for Grid..."
sleep 5
done
- name: Run tests
run: mvn test -Dselenium.hub.url=http://localhost:4444/wd/hubTracking which tests fail on which browser, and whether failures are consistent or intermittent, is where HelpMeTest adds value — it gives you historical visibility into cross-browser reliability so you can prioritize which failures are real bugs versus infrastructure noise.
Summary
Selenium Grid 4 is the right choice when you need cross-browser testing at scale. The architecture is more complex than modern tools like Playwright, but it handles browser variety that Playwright doesn't support (Safari, IE, old browser versions via custom nodes).
Key takeaways:
- Use Hub mode in CI; use distributed mode only when scaling beyond a single machine
- Always set
shm_size: 2g— Chrome crashes without it - Zalenium adds video recording and dynamic scaling with minimal configuration overhead
- Kubernetes deployment with KEDA autoscaling handles variable load efficiently
- Monitor the session queue length — if it's consistently non-zero, add more nodes
- Set
SE_SESSION_REQUEST_TIMEOUTappropriately — default is 5 minutes, which may be too long for fast CI pipelines