Selenium Grid Architecture, Roles, Routing, and Session Distribution: Guided Hands-On Workflow
This guided workflow turns the architecture into observable state. You will start one local Standalone Grid, inspect it before mutation, create a RemoteWebDriver session against a loopback AUT, correlate the session with Grid status/GraphQL, then intentionally request an impossible browser version so the queue/distribution failure can be observed rather than guessed.
Learning objectives
- Start a disposable Selenium Server 4.47.0 Standalone using loopback networking.
- Inspect Grid health, slots, stereotypes, and queue state before creating a session.
- Create a Python RemoteWebDriver session and capture returned capabilities/session ID.
- Use GraphQL/status to connect the session to Grid capacity and placement.
- Generate and interpret a bounded unsatisfied-capability request without blanket retries.
1. Build the local AUT
The following example makes the Build the local AUT behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from pathlib import Path
ROOT = Path("chapter19_fixture")
SITE = ROOT / "site"
SITE.mkdir(parents=True, exist_ok=True)
(SITE / "index.html").write_text(r"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Grid Routing Lab</title><style>body{font-family:system-ui;margin:32px;max-width:800px}.card{border:1px solid #888;border-radius:12px;padding:16px;margin:16px 0}button{padding:9px 13px}</style></head>
<body><h1 data-testid="heading">Grid Routing Lab</h1><div class="card"><p data-testid="state" data-state="ready">ready</p><button data-testid="advance" type="button">Advance state</button></div>
<script>
const state=document.querySelector('[data-testid="state"]');
document.querySelector('[data-testid="advance"]').addEventListener('click',()=>{state.dataset.state='advanced';state.textContent='advanced'});
</script></body></html>""", encoding="utf-8")
(ROOT / "serve.py").write_text(r"""from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
ROOT=Path(__file__).resolve().parent; SITE=ROOT/"site"; HOST="127.0.0.1"; PORT=8784
class H(SimpleHTTPRequestHandler):
def translate_path(self,path):
raw=super().translate_path(path); rel=Path(raw).relative_to(Path.cwd()); return str(SITE/rel)
def log_message(self,fmt,*args): pass
server=ThreadingHTTPServer((HOST,PORT),H); print(f"Grid fixture: http://{HOST}:{PORT}/")
try: server.serve_forever()
except KeyboardInterrupt: pass
finally: server.server_close()
""", encoding="utf-8")
print(ROOT.resolve())
The following example makes the Build the local AUT behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python build_fixture.py
python chapter19_fixture/serve.py
The AUT listens only on 127.0.0.1:8784. This works for
the mandatory Standalone lab because the browser Node is on the same
machine. If Nodes live on other machines or containers,
127.0.0.1 means those Nodes themselves; use a private
address that the browser host can reach instead.
2. Preflight Java, browser, server, and port ownership
Current Selenium Grid 4.47 requires Java 11+ for the server
quick-start path. Use the official 4.47.0 server JAR and at least
one installed desktop browser. Grid can use drivers on
PATH; the current quick-start also documents
--selenium-manager true so the server can configure
drivers when they are not already available.
java -version
python --version
python -m pip install "selenium==4.47.0"
# Confirm the exact server file you downloaded from selenium.dev/downloads.
ls selenium-server-4.47.0.jar
Keep Router/Grid endpoints on loopback/private networking. Selenium documentation explicitly warns that an exposed Grid can let outsiders drive browsers, access internal applications/files, or run custom binaries.
3. Start Standalone with a short lab queue timeout
Run this in its own terminal so logs remain visible. The short session-request timeout is deliberate for the failure exercise; production values require workload-specific policy.
java -jar selenium-server-4.47.0.jar standalone --selenium-manager true --session-request-timeout 8 --session-request-timeout-period 1 --port 4444
Expected: the Grid announces the Router/UI on
http://127.0.0.1:4444 and detects at least one browser
stereotype. Do not continue if the Grid has no usable slots.
4. Inspect capacity before the first session
The following example makes the Inspect capacity before the first session behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from urllib.request import urlopen
import json
with urlopen("http://127.0.0.1:4444/status", timeout=2) as r:
status = json.load(r)
print(json.dumps(status, indent=2))
Look for a ready Grid, registered Node information, slots, and
stereotypes. Save this as status-before.json if you
want an evidence packet. At this moment there should be no session
created by this lesson.
5. Create RemoteWebDriver and prove the AUT path
The following example makes the Create RemoteWebDriver and prove the AUT path behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
import json, selenium
from selenium import webdriver
from selenium.webdriver.common.by import By
options = webdriver.ChromeOptions()
driver = webdriver.Remote(command_executor="http://127.0.0.1:4444", options=options)
try:
driver.get("http://127.0.0.1:8784/")
print(json.dumps({
"selenium": selenium.__version__,
"session_id": driver.session_id,
"browserName": driver.capabilities.get("browserName"),
"browserVersion": driver.capabilities.get("browserVersion"),
"platformName": driver.capabilities.get("platformName"),
"url": driver.current_url,
"title": driver.title,
"state": driver.find_element(By.CSS_SELECTOR, '[data-testid="state"]').text,
}, indent=2))
driver.find_element(By.CSS_SELECTOR, '[data-testid="advance"]').click()
assert driver.find_element(By.CSS_SELECTOR, '[data-testid="state"]').get_attribute("data-state") == "advanced"
finally:
driver.quit()
webdriver.Remote changes where the browser session is
created, not what the AUT assertion means. The test runner still
owns the Python assertion and evidence path; Grid owns session
placement and routing.
6. Correlate the session with Grid GraphQL
Run the next version while keeping the session open only long enough to inspect placement. The GraphQL endpoint can return the owning Node and slot for a session ID.
import json
from urllib.request import Request, urlopen
from selenium import webdriver
options = webdriver.ChromeOptions()
driver = webdriver.Remote(command_executor="http://127.0.0.1:4444", options=options)
try:
sid = driver.session_id
query_text = """{
session(id: "%s") {
id nodeId nodeUri
slot { id stereotype lastStarted }
}
grid { sessionCount sessionQueueSize maxSession }
}""" % sid
query = {"query": query_text}
req = Request("http://127.0.0.1:4444/graphql",
data=json.dumps(query).encode(),
headers={"Content-Type": "application/json"})
with urlopen(req, timeout=3) as r:
print(json.dumps(json.load(r), indent=2))
finally:
driver.quit()
The important causal proof is the same session ID appearing with a
Node URI/slot. After quit(), a fresh query should show
the session gone and capacity released.
7. Inspect the New Session Queue directly
Current Grid exposes a read-only queue endpoint. With no blocked requests, it should be empty or report zero pending requests.
curl http://127.0.0.1:4444/se/grid/newsessionqueue/queue
This endpoint is operational evidence. Do not clear the queue as a troubleshooting shortcut; clearing it rejects waiting clients and destroys evidence about why they were waiting.
8. Intentionally request an unavailable stereotype/version
Use a browser family that your Grid actually advertises, but request an impossible version. The short lab timeout prevents a five-minute wait. Run this in one terminal and inspect the queue/status from another while it is pending.
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
options = webdriver.ChromeOptions()
options.browser_version = "999.0-NOT-INSTALLED"
try:
webdriver.Remote(command_executor="http://127.0.0.1:4444", options=options)
except WebDriverException as exc:
print(type(exc).__name__)
print(str(exc)[:800])
The following example makes the Intentionally request an unavailable stereotype/version behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
# While the request is pending:
curl http://127.0.0.1:4444/se/grid/newsessionqueue/queue
curl http://127.0.0.1:4444/status
Interpretation: the Router accepted a new-session request and the queue held it, but the Distributor could not find a free slot whose stereotype/capabilities matched the impossible version. The correction is to request a supported version or add appropriate capacity—not to restart the browser or increase a UI wait.
9. Challenge: choose the correct control
Your Grid has one Chrome slot and one Firefox slot. Chrome is busy; a new Chrome request waits while a Firefox request starts immediately. Which layer should you change if this is expected demand: locator code, AUT wait, Router, queue timeout, or Node capacity? Explain which observable state proves your answer before changing anything.
Target reasoning: the queued Chrome request plus occupied Chrome slot proves a capacity/matching constraint. Add or free matching Chrome capacity after measuring host resources; do not “fix” it with application waits.
10. Cleanup and rollback
Call driver.quit() in every script. Stop the AUT server
and Grid terminal with Ctrl+C. Then remove only the disposable lab
directory you created. Verify port 4444 no longer answers and no
browser processes from the lab remain.
from pathlib import Path
import shutil
p = Path("chapter19_fixture")
if p.exists():
shutil.rmtree(p)
print("fixture removed")
11. What the workflow proved
- Standalone exposes the full logical Grid architecture through one local process.
- RemoteWebDriver creates the browser through Grid, while test intent/assertions stay in the test runner.
- Session ID, returned capabilities, GraphQL placement, and Grid status provide causal routing evidence.
- An unavailable capability becomes queue/distribution evidence, not a generic “Selenium failed” message.
- Loopback AUT URLs are valid only because the Node/browser is local in this mandatory path.
Knowledge check
Why use webdriver.Remote in this lesson instead of
webdriver.Chrome()?
RemoteWebDriver sends the new-session request to Grid so Router/Queue/Distributor/Node routing is actually exercised.
What evidence distinguishes “all matching slots are busy” from “no matching stereotype exists”?
Inspect the requested capabilities, queue entry, Node/slot stereotypes, occupied sessions, and Grid status/GraphQL. Busy capacity shows matching stereotypes with no free capacity; unavailable stereotype/version shows no suitable match.
Why is 127.0.0.1:8784 unsafe as a universal AUT
URL for a remote Grid?
Loopback is relative to the browser/Node host. A Node on another machine or container would resolve it to itself, not to the test runner fixture host.
What should happen to Grid capacity after
driver.quit()?
The browser session ends, its Session Map entry is removed, and the owning slot becomes free for another matching request.
Why is clearing the queue not the first troubleshooting action?
It rejects waiting requests and destroys useful evidence. Diagnose matching, capacity, health, and timeout state first.
Official references and version notes
- Selenium 4.47 release notes — current stable client and Grid baseline.
- Selenium downloads — Python and Selenium Server/Grid 4.47.0 stable releases.
- Selenium Grid — purpose and current Grid documentation entry point.
- Grid components — Router, Distributor, Session Map, New Session Queue, Node, and Event Bus responsibilities.
- Grid architecture — slots, stereotypes, sessions, and logical relationships.
- Grid getting started — Standalone, Hub/Node, Distributed roles, ports, Java/browser prerequisites, and Selenium Manager option.
- Grid CLI options — current Node/session queue/capacity/heartbeat/BiDi/managed-download options.
- Grid TOML configuration — reviewable configuration examples and Router authentication.
- Grid endpoints — status, Node, session, and New Session Queue endpoints.
- Grid GraphQL support — session placement, Node, queue, and capacity queries.
- External datastore — JDBC/Redis-backed Session Map patterns.
- Grid configuration help — use the pinned server JAR help as implementation-grounded configuration truth.
Version-sensitive behavior was rechecked against Selenium primary
documentation on 2026-08-28. Mandatory examples pin Selenium
Python and Selenium Server/Grid 4.47.0, Python 3.10+, and Java 11+
for the server path. Local Standalone/Hub/Node labs remain on
loopback/private networking and use Selenium Manager on the Grid
Node only through the current documented
--selenium-manager true option. Queue timeouts are
deliberately shortened only for disposable failure exercises. Grid
4 roles are taught as Router, New Session Queue, Distributor,
Session Map, Event Bus, Nodes, slots/stereotypes; Standalone
packages rather than replaces those responsibilities. External
state backends are optional advanced architecture and must be
revalidated against the pinned server/API before production use.
Paid browser clouds, public Grid endpoints, enterprise
identity/proxy infrastructure, and managed Kubernetes/cloud are
not required.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.