Distributed Testing with Selenium Grid and Zalenium

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 count

For 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:4443

For 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:4444

Docker 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=20

Start 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 8

Key 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 ends

Zalenium'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 here

Key metrics to track:

  • selenium_grid_session_active — current active sessions
  • selenium_grid_session_queue_size — sessions waiting for a node
  • selenium_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-namespace

The 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.com
helm install selenium-grid docker-selenium/selenium-grid \
  -f selenium-values.yaml \
  --namespace selenium

CI 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/hub

Tracking 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_TIMEOUT appropriately — default is 5 minutes, which may be too long for fast CI pipelines

Read more

Start now free