QA How-To
Selenium 4 Grid Session Queue Monitoring Tutorial (2026)
Follow this Selenium 4 Grid session queue monitoring tutorial to export queue depth, graph pressure in Prometheus, and alert before tests time out.
18 min read | 2,401 words
TL;DR
Run Selenium Grid 4.46.0, query `grid.sessionQueueSize` through `/graphql`, and expose that value to Prometheus with a small exporter. Alert only when the queue remains nonzero long enough to affect test startup, then use Grid status and node logs to distinguish insufficient capacity from unhealthy nodes.
Key Takeaways
- Read queue depth from Grid's GraphQL endpoint instead of scraping the console UI.
- Export queue depth and Grid readiness as low-cardinality Prometheus gauges.
- Test monitoring with more simultaneous requests than available browser slots.
- Alert on sustained queue pressure, not a harmless one-scrape burst.
- Correlate queue size with test wait time, node health, and configured capacity.
- Protect the Grid endpoint and pin container versions in shared environments.
A Selenium 4 Grid session queue monitoring tutorial should answer one operational question: are tests waiting because every compatible browser slot is busy? Query Grid's GraphQL API, turn sessionQueueSize into a Prometheus metric, and alert on sustained pressure rather than watching the Grid console by hand.
This guide builds that path locally with pinned 2026 versions. You will deliberately create contention, prove that the metric changes, and leave with an alert that distinguishes a transient burst from a capacity incident. If Grid itself is new to you, read Docker for Selenium Grid before applying this setup to CI.
TL;DR: Selenium 4 Grid Session Queue Monitoring Tutorial
| Signal | Source | Meaning | First response |
|---|---|---|---|
selenium_grid_session_queue_size |
Grid GraphQL | Requests waiting for a compatible slot | Check active capacity and node stereotypes |
selenium_grid_ready |
Grid /status |
Router considers the Grid ready | Inspect Grid and node logs |
up{job="selenium-grid-exporter"} |
Prometheus scrape | Exporter is reachable | Repair exporter network or process |
| Queue age in CI | Test runner timestamps | User-visible startup delay | Add compatible slots or reduce concurrency |
A queue size of zero is not the goal at every instant. A short queue can be normal when a test wave starts. The actionable condition is a nonzero or growing queue that persists beyond your accepted session-start delay.
What You Will Build
You will create a small, observable Grid stack with:
- Selenium Grid 4.46.0 in Hub and Chrome Node containers.
- Two total Chrome session slots, making queue behavior easy to reproduce.
- A Python 3.13 exporter that reads Grid GraphQL and
/status. - Prometheus 3.5.0 scraping the exporter every five seconds.
- A rule that warns after five minutes of continuous queued work.
- Verification commands for the raw API, exported metrics, Prometheus targets, and alert logic.
The exporter has no Selenium client dependency. It uses documented HTTP surfaces, so Java, Python, JavaScript, and .NET test suites all produce the same infrastructure signal.
Prerequisites
Use Docker Engine 28.3 or later with Docker Compose v2.38 or later. The examples pin Selenium Server and Docker Selenium images to 4.46.0-20260711, Python to 3.13.5-slim, and Prometheus to v3.5.0. Run docker version, docker compose version, and curl --version to confirm the tools exist.
Create an empty working directory outside your application repository:
mkdir grid-queue-monitor && cd grid-queue-monitor
mkdir exporter prometheus
The lab uses ports 4444 for Grid, 8000 for exporter metrics, and 9090 for Prometheus. Stop any process already bound to those ports. Allocate at least 4 GB of Docker memory because two Chrome sessions plus Grid and Prometheus run concurrently. On Linux, make sure the Docker daemon is running; on macOS or Windows, start Docker Desktop.
Verification: docker info must return server details, and docker compose version must print a v2 version rather than the retired standalone docker-compose v1 command.
Step 1: Start a Grid With Deliberately Limited Capacity
Create compose.yaml. The Hub owns routing and the new-session queue. The Chrome Node registers two slots through SE_NODE_MAX_SESSIONS=2; keeping SE_NODE_OVERRIDE_MAX_SESSIONS=false prevents accidental oversubscription beyond available processors.
services:
selenium-hub:
image: selenium/hub:4.46.0-20260711
container_name: selenium-hub
ports:
- "4444:4444"
environment:
SE_SESSION_REQUEST_TIMEOUT: 300
SE_SESSION_RETRY_INTERVAL: 5
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4444/status"]
interval: 5s
timeout: 3s
retries: 20
chrome:
image: selenium/node-chrome:4.46.0-20260711
shm_size: 2gb
depends_on:
selenium-hub:
condition: service_healthy
environment:
SE_EVENT_BUS_HOST: selenium-hub
SE_EVENT_BUS_PUBLISH_PORT: 4442
SE_EVENT_BUS_SUBSCRIBE_PORT: 4443
SE_NODE_MAX_SESSIONS: 2
SE_NODE_OVERRIDE_MAX_SESSIONS: "false"
SE_NODE_SESSION_TIMEOUT: 120
Start it and wait for registration:
docker compose up -d selenium-hub chrome
curl -fsS http://localhost:4444/status
The request timeout is intentionally five minutes for the exercise. A queued request is rejected after that limit, while the retry interval controls how frequently the queue rechecks whether a matching slot has appeared. In production, align the request timeout with the test runner's client timeout so the client does not abandon a request while Grid still holds it.
Verification: the status JSON must contain "ready": true. Open http://localhost:4444/ui and confirm one Chrome Node with two slots. For a broader deployment walkthrough, compare running Selenium Grid in Kubernetes.
Step 2: Query the Session Queue Through GraphQL
Do not scrape numbers from /ui. The UI is for people and its markup can change. Grid's /graphql endpoint provides machine-readable state. Send the query in a JSON body so braces and whitespace are handled predictably:
curl -fsS \
-H 'Content-Type: application/json' \
--data '{"query":"{ grid { sessionQueueSize } }"}' \
http://localhost:4444/graphql
With no waiting requests, the response should resemble:
{"data":{"grid":{"sessionQueueSize":0}}}
GraphQL may return HTTP 200 while placing a query error in an errors array. Monitoring code therefore must validate both the HTTP response and the data shape. Treat a missing data.grid.sessionQueueSize as a failed observation, never as queue size zero. Otherwise a schema, authentication, or proxy error creates a dangerously reassuring chart.
The queue contains new-session requests, not running sessions. A request stays there until the Distributor finds a free slot whose stereotype matches the requested capabilities. Two free Firefox slots do not drain a queue of Chrome requests. This distinction is essential when a dashboard says capacity exists but the requested browser, platform, or version does not.
Verification: run the command twice and confirm an integer is returned each time. A 404 usually means a reverse proxy has not forwarded /graphql; HTML usually means the proxy rewrote the route to a login or landing page.
Step 3: Build a Defensive Prometheus Exporter
Create exporter/requirements.txt:
prometheus-client==0.22.1
requests==2.32.4
Create exporter/exporter.py:
import os
import time
import requests
from prometheus_client import Gauge, start_http_server
GRID_URL = os.getenv("GRID_URL", "http://selenium-hub:4444").rstrip("/")
POLL_SECONDS = float(os.getenv("POLL_SECONDS", "5"))
TIMEOUT_SECONDS = float(os.getenv("HTTP_TIMEOUT_SECONDS", "3"))
queue_size = Gauge(
"selenium_grid_session_queue_size",
"New session requests waiting for a compatible Selenium Grid slot.",
)
grid_ready = Gauge(
"selenium_grid_ready",
"1 when Selenium Grid status reports ready, otherwise 0.",
)
collection_success = Gauge(
"selenium_grid_collection_success",
"1 when the most recent Grid collection completed successfully.",
)
session = requests.Session()
def collect() -> None:
status_response = session.get(f"{GRID_URL}/status", timeout=TIMEOUT_SECONDS)
status_response.raise_for_status()
status = status_response.json()
graph_response = session.post(
f"{GRID_URL}/graphql",
json={"query": "{ grid { sessionQueueSize } }"},
timeout=TIMEOUT_SECONDS,
)
graph_response.raise_for_status()
graph = graph_response.json()
if graph.get("errors"):
raise RuntimeError(f"GraphQL errors: {graph['errors']}")
value = graph["data"]["grid"]["sessionQueueSize"]
if not isinstance(value, int) or value < 0:
raise ValueError(f"Invalid sessionQueueSize: {value!r}")
grid_ready.set(1 if status.get("value", {}).get("ready") else 0)
queue_size.set(value)
collection_success.set(1)
def main() -> None:
start_http_server(8000)
while True:
started = time.monotonic()
try:
collect()
except (requests.RequestException, KeyError, TypeError, ValueError, RuntimeError) as error:
collection_success.set(0)
grid_ready.set(0)
print(f"collection failed: {error}", flush=True)
elapsed = time.monotonic() - started
time.sleep(max(0.1, POLL_SECONDS - elapsed))
if __name__ == "__main__":
main()
Create exporter/Dockerfile:
FROM python:3.13.5-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY exporter.py .
USER 65532:65532
CMD ["python", "exporter.py"]
The gauges have no labels. That is intentional: browser, build, session ID, and test name labels would create unbounded time series. Detailed test identity belongs in CI results or traces, while infrastructure metrics should remain cheap to aggregate.
Append the exporter service to compose.yaml:
grid-exporter:
build: ./exporter
ports:
- "8000:8000"
depends_on:
selenium-hub:
condition: service_healthy
environment:
GRID_URL: http://selenium-hub:4444
POLL_SECONDS: 5
HTTP_TIMEOUT_SECONDS: 3
Verification: run docker compose up -d --build grid-exporter, then curl -fsS http://localhost:8000/metrics | grep selenium_grid. All three metric families should appear, collection success should equal 1, and queue size should initially equal 0.
Step 4: Configure Prometheus and a Queue Alert
Create prometheus/prometheus.yml:
global:
scrape_interval: 5s
evaluation_interval: 5s
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: selenium-grid-exporter
static_configs:
- targets: ["grid-exporter:8000"]
Create prometheus/alerts.yml:
groups:
- name: selenium-grid
rules:
- alert: SeleniumGridSessionQueueStalled
expr: selenium_grid_session_queue_size > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Selenium Grid has queued session requests"
description: "{{ $value }} new sessions are waiting for compatible slots."
- alert: SeleniumGridExporterCannotCollect
expr: selenium_grid_collection_success == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Selenium Grid metrics collection is failing"
Append Prometheus to compose.yaml:
prometheus:
image: prom/prometheus:v3.5.0
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
depends_on:
- grid-exporter
A five-minute for period suppresses momentary startup waves. Tune it against your service-level objective: if sessions are expected to start within 60 seconds, five minutes is too forgiving. Add routing through Alertmanager in production; Prometheus evaluates rules but does not by itself deliver Slack, email, or paging notifications.
Verification: start the service with docker compose up -d prometheus. Visit http://localhost:9090/targets; the exporter target must be UP. Visit /rules and confirm both rules load without an error.
Step 5: Create More Session Requests Than Available Slots
Use a disposable Python client to launch four sessions concurrently while each session holds its slot for 45 seconds. Create load.py in the lab root:
from concurrent.futures import ThreadPoolExecutor
import time
from selenium import webdriver
GRID = "http://localhost:4444"
def occupy_slot(worker: int) -> None:
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Remote(command_executor=GRID, options=options)
try:
driver.get("data:text/html,<title>Grid queue lab</title><h1>worker</h1>")
assert driver.title == "Grid queue lab"
print(f"worker {worker} acquired {driver.session_id}", flush=True)
time.sleep(45)
finally:
driver.quit()
with ThreadPoolExecutor(max_workers=4) as pool:
list(pool.map(occupy_slot, range(1, 5)))
Run it in a virtual environment with the binding version matching Grid:
python3.13 -m venv .venv
. .venv/bin/activate
pip install selenium==4.46.0
python load.py
The first two workers acquire the two Chrome slots. The other two webdriver.Remote constructors block while their new-session requests wait. When the first pair quits, the queued pair should acquire the released slots. Do not use this load generator against a shared production Grid because it intentionally consumes capacity.
In another terminal, sample the raw queue while the script runs:
watch -n 2 "curl -sS -H 'Content-Type: application/json' --data '{\"query\":\"{ grid { sessionQueueSize } }\"}' http://localhost:4444/graphql"
Verification: observe queue size rise to 2, later fall to 0, and see four acquired session IDs in the load process. Exact timing varies because container startup and scheduling are asynchronous.
Step 6: Apply the Selenium 4 Grid Session Queue Monitoring Tutorial Metrics
Open Prometheus at http://localhost:9090/graph and query:
selenium_grid_session_queue_size
Switch to the graph view. The line should step from zero to roughly two during the contention period and return to zero after sessions start. Next, inspect the peak over a rolling window:
max_over_time(selenium_grid_session_queue_size[15m])
For a less noisy operational panel, graph a one-minute average:
avg_over_time(selenium_grid_session_queue_size[1m])
Queue depth alone cannot tell you how long a particular request waited. Record session-request start and successful driver construction timestamps in your test harness, then publish a histogram such as grid_session_start_seconds. Correlate its p95 with queue depth. That reveals whether a queue of three is harmless because slots turn over quickly or damaging because sessions last 30 minutes.
A useful dashboard places queue size beside active session count, registered slot count, Grid readiness, node CPU, node memory, and session-start latency. For distributed telemetry beyond these core metrics, follow the Selenium Grid OpenTelemetry monitoring setup.
Verification: set the time range to the last 15 minutes and confirm the peak query reports a value above zero from Step 5. If the instant query is zero, that is expected after the test finishes; use the range query to retain evidence of the burst.
Step 7: Turn the Signal Into a Capacity Decision
Interpret a sustained queue with compatible capacity, not in isolation. Use this decision table during an incident:
| Observation | Likely cause | Action |
|---|---|---|
| Queue grows, all matching slots busy | Legitimate capacity shortage | Add nodes, reduce runner concurrency, or shorten sessions |
| Queue grows, matching slots look free | Node registration or Distributor health issue | Check /status, Hub logs, and node event-bus connectivity |
| Only one browser queues | Missing browser-specific capacity | Scale that stereotype rather than every node |
| Queue alternates rapidly around zero | Normal burst scheduling | Increase alert duration, keep trend for planning |
| Collection fails but tests run | Exporter, proxy, auth, or schema issue | Repair observability before drawing capacity conclusions |
Set CI concurrency from measured slot supply. If a Grid has 20 Chrome slots but reserves four for release verification, cap the general pipeline at 16 concurrent Chrome sessions. This is an operating policy, not a guarantee: unhealthy nodes and capability constraints can reduce usable supply. In elastic environments, a queue can be the scale-out trigger, but also set maximum replicas and cooldown behavior to control cost and oscillation. See Selenium Grid Kubernetes autoscaling step by step and Selenium Grid cloud scaling for those deployment models.
Protect /graphql, /status, exporter metrics, and the Grid UI behind an internal network or authenticated proxy. Metrics reveal utilization and operational state. Configure TLS at ingress, allow only monitoring networks, and avoid exposing port 4444 directly to the internet.
Review the signal by pipeline class too. A nightly regression wave may tolerate several minutes of waiting, while a pull-request smoke suite may need a slot in seconds. Keep one infrastructure queue gauge, but calculate session-start latency in each runner. Add bounded labels such as pipeline class only when the value set is controlled. This preserves each team service-level view without multiplying Grid metrics.
Capacity planning needs several representative weeks, including release days. Record peak queue depth, queue episode duration, session runtime, and healthy compatible slots. One peak proves demand exceeded supply at one moment; it does not prove permanent nodes are economical. Frequent long episodes justify baseline capacity. Rare predictable spikes may be better served by scheduled or queue-driven scaling. Re-run the load exercise after every scaling change to prove requests drain and scale-down does not terminate active sessions.
Verification: write down the slot budget, accepted session-start delay, warning threshold, critical threshold, and owner. A technically valid rule without an owner or response action is only a chart annotation.
Troubleshooting
Problem: /graphql returns 404 or an HTML document -> Query the Router on port 4444, not a Node port. Configure the reverse proxy to preserve POST requests and forward /graphql without rewriting it to /ui.
Problem: GraphQL returns an errors array -> Run the minimal { grid { sessionQueueSize } } query directly against Grid. Check the deployed Grid version and do not silently substitute zero when the response lacks the expected field.
Problem: the exporter metric never rises during the load test -> Confirm SE_NODE_MAX_SESSIONS is 2, verify only one Chrome Node is registered, and start at least four requests at once. Sequential tests never queue because each releases its slot before the next request.
Problem: sessions time out before a slot becomes free -> Increase SE_SESSION_REQUEST_TIMEOUT or reduce the simulated hold time. Also check the WebDriver client's HTTP timeout, which must exceed the acceptable queue wait. Treat a longer timeout as protection from bursts, not a substitute for capacity.
Problem: Prometheus target is DOWN while localhost:8000/metrics works -> Prometheus runs inside Compose, where localhost points to its own container. Set the target to grid-exporter:8000 and confirm both services share the default Compose network.
Problem: queue exists while the Grid UI shows empty slots -> Compare requested capabilities with slot stereotypes. Browser name, platform name, browser version constraints, and custom capability matching can make visually free slots incompatible. Inspect Hub and Distributor logs before scaling unrelated browsers.
Best Practices
- Pin Selenium and Docker image versions, then upgrade the Grid and client bindings through a controlled compatibility test.
- Alert separately on collection failure. A stale queue gauge must not masquerade as healthy zero demand.
- Keep metric labels bounded; never attach session IDs, test names, URLs, or commit hashes to infrastructure gauges.
- Measure session-start latency in the test runner because queue depth is not a duration.
- Use a
forduration based on tolerated delay and normal pipeline bursts. - Drain nodes before maintenance so in-flight sessions finish and new requests route elsewhere.
- Close every driver in
finallyor fixture teardown; leaked sessions occupy slots and manufacture queue pressure.
Interview Questions and Answers
The model answers are included in the structured interview section below. The strongest explanation connects new-session requests, capability matching, Distributor behavior, and slot supply instead of defining queue size as a generic load number.
Where To Go Next
Keep the exporter running through several real CI peaks and compare queue history with session-start latency. Then set thresholds from evidence, document who responds, and test the alert by temporarily lowering its for duration in a nonproduction Grid.
Deepen the setup with dynamic Selenium Grid nodes in Docker when fixed browser containers waste resources. Add traces with the Selenium Grid OpenTelemetry guide, or move the capacity policy to Kubernetes Grid autoscaling. To sharpen framework-side cleanup and concurrency controls, use build a Selenium Java framework from scratch. You can also assess your broader automation profile in the QAJobFit resume dashboard and rehearse related scenarios in QA practice.
Conclusion
Reliable Selenium Grid queue monitoring starts with one authoritative value and the context required to interpret it. Read sessionQueueSize from GraphQL, expose collection health alongside it, retain history in Prometheus, and alert only when waiting work persists beyond your agreed session-start objective.
The load exercise proves the complete path from client contention to a visible metric. Once it works, add session-start latency and compatible slot supply so the team can decide whether to scale, repair unhealthy nodes, or correct an overly aggressive CI concurrency setting.
Interview Questions and Answers
What is the new-session queue in Selenium Grid 4?
It holds WebDriver new-session requests until the Distributor can assign a compatible free slot. Running sessions are not queue entries. A request can wait even when unrelated browser slots are idle because capability matching is part of assignment.
How would you monitor Selenium Grid queue depth?
I would query `grid.sessionQueueSize` through the Router's GraphQL endpoint, export it as a low-cardinality gauge, and store it in Prometheus. I would publish collection success separately and correlate queue depth with session-start latency and compatible slot count.
Why should monitoring code check GraphQL errors on an HTTP 200 response?
GraphQL can report query failures in an `errors` field while the HTTP request itself succeeds. Reading only the status code could turn a missing value into false health. The collector should validate the response shape and mark collection failed without overwriting the gauge with zero.
How do you avoid noisy queue alerts?
I use a `for` duration that exceeds ordinary test-wave bursts and derive it from the session-start objective. I also distinguish exporter failure from actual queue pressure. Trend and latency panels provide context before the alert reaches an on-call engineer.
What would you investigate if Grid has queued Chrome requests and free Firefox slots?
That is expected because Firefox slots cannot satisfy Chrome capabilities. I would inspect Chrome node registration, health, stereotypes, and active Chrome sessions. Scaling Firefox would add cost without draining the queue.
What metrics belong beside Selenium Grid queue size?
I want session-start latency, active sessions, registered slots by bounded stereotype, Grid readiness, collection success, and node CPU and memory. Queue depth describes waiting work, while those signals explain impact and whether supply is healthy.
Frequently Asked Questions
How do I check the Selenium Grid session queue size?
POST the GraphQL query `{ grid { sessionQueueSize } }` to the Grid Router's `/graphql` endpoint. Validate the returned integer and also fail collection when GraphQL includes an `errors` array.
Does a nonzero Selenium Grid queue always mean there is a problem?
No. Short queues are normal when many pipelines begin together and slots turn over quickly. Alert when the queue persists beyond the session-start delay your team accepts, and correlate it with actual wait duration.
Why are sessions queued when Selenium Grid shows free slots?
Free slots may not match the requested browser, platform, version, or custom capabilities. Check the request against node stereotypes and inspect Distributor logs before assuming total Grid capacity is insufficient.
Can Prometheus scrape Selenium Grid queue size directly?
The tutorial uses a small exporter because the queue value is available through GraphQL and needs conversion into Prometheus text format. The exporter also validates the response and publishes collection health, preventing failed queries from appearing as a zero queue.
What alert threshold should I use for the session queue?
Start from your accepted session-start delay rather than copying a universal number. If CI sessions must begin within two minutes, warn before sustained queueing can violate that objective and use a shorter critical threshold when the queue is growing.
How can I reduce Selenium Grid session queue time?
Add slots that match queued capabilities, cap CI concurrency, shorten unnecessarily long sessions, and fix leaked drivers. In elastic infrastructure, scale on queue pressure while setting sensible replica limits and cooldown periods.
Related Guides
- Monitor Selenium Grid with OpenTelemetry: Selenium Grid OpenTelemetry Monitoring Setup
- How to Use Selenium DevTools in Selenium 4 in Java (2026)
- Selenium 4 BiDi Download Progress Testing Tutorial (2026)
- Selenium DevTools in Selenium 4 in Python (2026)
- Running tests on a Selenium Grid in Kubernetes (2026)
- Selenium Grid Cloud Scaling Complete Guide (2026)