Chapter 19Lesson 02~255 minutes

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.

Hands-on GridRemoteWebDriverGrid statusGraphQLQueue diagnostics

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
Do not expose this lab Grid

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()?

What evidence distinguishes “all matching slots are busy” from “no matching stereotype exists”?

Why is 127.0.0.1:8784 unsafe as a universal AUT URL for a remote Grid?

What should happen to Grid capacity after driver.quit()?

Why is clearing the queue not the first troubleshooting action?

Next lesson

Selenium Grid Architecture, Roles, Routing, and Session Distribution: Configuration, Design Patterns, and Trade-Offs

Continue with Selenium Grid Architecture, Roles, Routing, and Session Distribution: Configuration, Design Patterns, and Trade-Offs. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.