PRACTICAL GUIDE / Selenium Grid custom node slot matcher

Create Custom Selenium Grid Node Slot Matchers

Learn Selenium Grid custom node slot matcher with working code, failure cases, debugging steps, and CI evidence for reliable QA automation in practice.

By The Testing AcademyUpdated July 18, 202618 min read
All field guides
In this guide10 sections
  1. Decide Whether a Custom Matcher Is Necessary
  2. Define the Capability Contract and Failure Policy
  3. Implement the Matcher as a Narrow Delegate
  4. Package and Load the Extension on the Right Role
  5. Test the Truth Table Before Starting Grid
  6. Verify Placement Through a Disposable Grid
  7. Roll Out Without Losing Capacity
  8. Debug Loading, Matching, and Allocation Separately
  9. Frequently Asked Questions
  10. Where does a custom Selenium Grid SlotMatcher run?
  11. Should a custom matcher replace DefaultSlotMatcher completely?
  12. Can a custom capability be used for Grid authorization?
  13. How should unsupported custom capabilities fail?
  14. How do I verify that Grid loaded the custom matcher?
  15. What should be tested after a Selenium Grid upgrade?
  16. Practice the Scheduler Contract

What you will learn

  • Decide Whether a Custom Matcher Is Necessary
  • Define the Capability Contract and Failure Policy
  • Implement the Matcher as a Narrow Delegate
  • Package and Load the Extension on the Right Role

A Selenium Grid custom node slot matcher should preserve Selenium's default capability rules, add one narrow namespaced scheduling invariant, load on the Distributor through an extension JAR, and pass positive and negative placement tests. Node stereotypes provide the offered facts; the Distributor's matcher decides whether each new-session request can use a slot.

The title often leads teams to put custom logic inside every Node. The real boundary is more precise. Nodes register slots and stereotypes. The Distributor maintains the Grid Model, asks a SlotMatcher whether a stereotype supports requested capabilities, and then lets a slot selector choose among matches. Standalone and Hub contain that Distributor role, so they load the matcher in-process.

The official SlotMatcher Java API defines one method: matches(Capabilities stereotype, Capabilities capabilities). That small surface can change allocation for every session, which makes a constrained contract, a complete truth table, and a reversible rollout more valuable than a large framework around it.

Decide Whether a Custom Matcher Is Necessary

Start with the supported configuration path. Selenium's CLI options reference shows custom namespaced capabilities in Node driver stereotypes and in every matching session request. For many teams, that configuration plus the default matcher is enough. A custom matcher is justified when default behavior intentionally ignores an extension capability that your scheduling policy must compare.

The official DefaultSlotMatcher documentation states that extension capabilities are not considered because their matching is implementation-specific. It still enforces standard non-extension capabilities and the required browser name, browser version, and platform behavior. A custom matcher can delegate to it and add the missing domain rule.

Use this decision table before writing Java:

RequirementBest mechanismWhy
Select Chrome instead of FirefoxStandard browserName stereotypeBuilt-in matching already owns it
Select an exact supported browser versionStandard browser version capability and versioned NodeAvoid duplicate rules
Prefer one equal matching NodeSlotSelector, topology, or capacity policyPreference is selection, not compatibility
Require a GPU execution tierNamespaced stereotype plus custom matcherExtension capability needs explicit semantics
Restrict a tenant to private capacityAuthentication and authorization outside capabilitiesClients control request capabilities
Provision a missing Node on demandDynamic Node or platform integrationA matcher does not create capacity
Route by region at the entry pointRegional Router or traffic policyKeep global network routing outside slot compatibility

A matcher answers a Boolean compatibility question. It does not rank matching slots, start a Node, authorize a user, reserve a quota, or prove that the browser launches. If the requirement uses words such as "prefer," "create," "permit this identity," or "send to the nearest region," place it at the owning boundary instead. The multi-region architecture guide illustrates why geographic routing should not be hidden inside a capability comparison.

Write a short decision statement: "A request carrying qa:executionTier=gpu may match only an UP slot whose stereotype carries the same normalized string, and generic requests may not consume tier-reserved slots." That statement is testable, leaves no hidden fallback, and identifies which side owns each value.

Collect a baseline before changing compatibility. Export the registered stereotypes, a representative sample of sanitized new-session capability shapes, queue wait by shape, and actual Node placements. Classify each historical request with the proposed rule offline. Any request that moves from supported to unsupported, or from generic to reserved capacity, needs an explicit migration decision. This comparison catches client assumptions that no design document lists.

Reject custom logic when the classification cannot be explained from the request and stereotype alone. A matcher that depends on current team quota, a database flag, or an incident toggle is really a policy service hidden in a synchronous allocation callback. Put those changing decisions before Grid or in capacity orchestration, then send a stable allowed scheduling fact to the matcher.

Define the Capability Contract and Failure Policy

Use a vendor-prefixed extension name containing a colon, such as qa:executionTier. Do not invent an unprefixed capability that can collide with WebDriver standard names. Publish the allowed values and their case policy. Keep values small and stable: standard, gpu, or restricted-network is easier to review than a nested object assembled by several clients.

Define absent and malformed behavior separately. In this article's reserved-lane policy, both absent means a generic slot can serve a generic request. Exactly one absent means no match. A non-string or blank value means no match. Two normalized equal strings mean a match, provided Selenium's default matcher also passes. This creates a four-way truth table rather than relying on Java object equality by accident.

Capability values are claims from two sources. The platform controls the Node stereotype through reviewed configuration. The test client controls its request. Matching those claims does not prove the client is entitled to the resource. If GPU minutes, private network access, or licensed software need protection, authorize the caller before its request reaches shared capacity. Never place a token or secret inside a stereotype because GraphQL, status, logs, and the Grid UI may expose capabilities.

Choose the unsupported-request policy from topology. In a static Grid, the Distributor option --reject-unsupported-caps true can reject capability shapes that no registered Node supports instead of leaving them waiting. In a dynamic Grid, an initially unsupported shape may be the trigger for provisioning, so immediate rejection can be incorrect. Align Grid queue timeout with client new-session timeout and document which layer returns the terminal error. The session queue timeout debugging guide provides the evidence chain for that decision.

Version the contract. Store the capability name, allowed values, normalization rule, absent rule, malformed rule, and owner beside the extension source. A browser image rollout that changes a stereotype should update this contract and its truth table in the same review.

Account for W3C capability negotiation. Clients can send alwaysMatch plus one or more firstMatch entries; the effective candidate capability sets are what Grid evaluates, not a visual reading of one JSON fragment. Test the exact language binding request or capture the sanitized new-session payload at the Router. Do not create custom semantics where an absent key in one firstMatch entry accidentally inherits a value your client never merged.

Treat normalization as part of public behavior. Trimming and lowercasing strings can make human-authored configuration friendlier, but it can also collapse values that a provider considers distinct. Never normalize URLs, opaque IDs, versions, or signed values without an owning specification. For a small enumerated tier, validate membership after normalization so a typo does not become an unlimited new scheduling class.

Implement the Matcher as a Narrow Delegate

The safest implementation calls the supported default matcher first. Only a standard-compatible slot reaches the custom rule. The following class has a public no-argument constructor, no mutable global state, and no network or file-system work in matches. Those properties make reflective loading and repeated allocation calls predictable.

Code 1: Java matcher for a reserved execution tier

Java
package com.qabattle.grid;

import java.util.Locale;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.grid.data.DefaultSlotMatcher;
import org.openqa.selenium.grid.data.SlotMatcher;

public final class ExecutionTierSlotMatcher implements SlotMatcher {
  private static final String CAPABILITY = "qa:executionTier";
  private final SlotMatcher standardMatcher = new DefaultSlotMatcher();

  @Override
  public boolean matches(Capabilities stereotype, Capabilities request) {
    if (!standardMatcher.matches(stereotype, request)) {
      return false;
    }

    Object offeredRaw = stereotype.getCapability(CAPABILITY);
    Object requestedRaw = request.getCapability(CAPABILITY);

    if (offeredRaw == null && requestedRaw == null) {
      return true;
    }
    if (!(offeredRaw instanceof String offered)
        || !(requestedRaw instanceof String requested)) {
      return false;
    }

    String normalizedOffered = offered.trim().toLowerCase(Locale.ROOT);
    String normalizedRequested = requested.trim().toLowerCase(Locale.ROOT);
    return !normalizedOffered.isEmpty()
        && normalizedOffered.equals(normalizedRequested);
  }
}

This code deliberately reserves tiered slots. If generic work should be allowed to use spare tiered capacity, change the absent-request branch only after defining starvation and cost behavior. That alternative is a policy change, not a cleanup refactor, and it needs load tests showing that high-value tier requests retain acceptable admission time.

Keep the custom rule deterministic. A matcher may be called frequently while requests wait and Node state changes. Calling a remote inventory service, reading a rotating file, or consulting the current clock can give the same request different compatibility answers and add latency to allocation. Put changing facts into reviewed Node stereotypes and update Nodes through their lifecycle instead.

Do not catch every exception and return true. A permissive fallback can send a request to incompatible or protected capacity. Validate the known input types, log extension load failures at startup, and let deployment fail before traffic when the class cannot be created.

Package and Load the Extension on the Right Role

Compile against the same Selenium Grid version that the server runs. Grid extension APIs can evolve, and a JAR compiled against another version can fail at class loading or, worse, behave differently after startup. The custom artifact should contain your class and its own unique dependencies, but it should not bundle a second conflicting copy of Selenium Grid classes.

Code 2: Maven dependency aligned to the Grid runtime

XML
<properties>
  <maven.compiler.release>17</maven.compiler.release>
  <selenium.version>4.46.0</selenium.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-grid</artifactId>
    <version>${selenium.version}</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

Use the version actually promoted by your platform; the value above is a concrete example, not permission to drift the extension and server independently. Record both artifact hashes in deployment evidence.

For Standalone, one TOML file can configure the in-process Distributor and local Node. The custom capability must exist in the Node stereotype because the matcher cannot infer hardware or network facts from a hostname.

Code 3: Standalone TOML and extension startup

TOML
[distributor]
slot-matcher = "com.qabattle.grid.ExecutionTierSlotMatcher"
reject-unsupported-caps = true

[node]
detect-drivers = false

[[node.driver-configuration]]
display-name = "Chrome GPU"
max-sessions = 1
stereotype = '{"browserName":"chrome","qa:executionTier":"gpu"}'
Shell
SE_VERSION=4.46.0
MATCHER_JAR=target/grid-slot-matcher-1.0.0.jar

java -jar "selenium-server-${SE_VERSION}.jar" \
  --ext "$MATCHER_JAR" \
  standalone --config grid.toml

In Hub and Node mode, load the JAR and [distributor] setting on the Hub process, then configure stereotypes on every relevant Node. In a fully distributed deployment, load them on the Distributor process. A Node does not need the matcher class merely to advertise qa:executionTier, although packaging standards may place a common extension set on all roles. The operational requirement is that the process constructing the Distributor can load the class.

The official architecture reference calls the stereotype the minimal capability set a request must match before allocation. Keep it minimal. Test names, build IDs, and tracing metadata belong in session capabilities but should not become slot-compatibility dimensions.

Test the Truth Table Before Starting Grid

Fast unit tests should cover every policy branch and prove default matching still applies. Include browser mismatch, platform mismatch, both tiers absent, equal tier with case normalization, unequal tier, one side absent, blank strings, and non-string values. Generate the matrix from named cases so reviewers can see what each Boolean means.

Code 4: JUnit tests for standard and extension rules

Java
package com.qabattle.grid;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.openqa.selenium.MutableCapabilities;

class ExecutionTierSlotMatcherTest {
  private final ExecutionTierSlotMatcher matcher =
      new ExecutionTierSlotMatcher();

  @Test
  void matchesEqualTierAfterNormalizingCase() {
    assertTrue(matcher.matches(
        capabilities("chrome", "GPU"),
        capabilities("chrome", "gpu")));
  }

  @Test
  void rejectsDifferentTier() {
    assertFalse(matcher.matches(
        capabilities("chrome", "gpu"),
        capabilities("chrome", "standard")));
  }

  @Test
  void rejectsGenericRequestForReservedSlot() {
    assertFalse(matcher.matches(
        capabilities("chrome", "gpu"),
        capabilities("chrome", null)));
  }

  @Test
  void stillRejectsBrowserMismatch() {
    assertFalse(matcher.matches(
        capabilities("chrome", "gpu"),
        capabilities("firefox", "gpu")));
  }

  private static MutableCapabilities capabilities(
      String browserName, String tier) {
    MutableCapabilities capabilities = new MutableCapabilities();
    capabilities.setCapability("browserName", browserName);
    if (tier != null) {
      capabilities.setCapability("qa:executionTier", tier);
    }
    return capabilities;
  }
}

Run mutation checks mentally or with tooling. If deleting the standardMatcher call leaves all tests green, the suite does not protect Selenium's core behavior. If returning true for a missing request tier leaves tests green, reserved capacity is not protected. If upper and lower case are intended to differ, remove normalization and encode that outcome explicitly.

Unit tests cannot prove reflective loading, class-path compatibility, Grid configuration, or actual placement. They establish only the pure compatibility contract. Keep that boundary visible in CI reports.

Add a generated pairwise matrix when the Node fleet supports several browsers, platforms, and tiers. The generator should enumerate approved values, then assert symmetry only where the contract calls for it. Stereotype and request are not generally interchangeable inputs, so avoid a generic mathematical symmetry assertion. Preserve named regression cases for every production routing incident even after broader generation covers the same values.

Run the unit suite against the candidate Selenium dependency during every upgrade pull request. Compile success is not enough because default matching behavior can evolve while the interface remains binary-compatible. A focused contract test that delegates to the candidate DefaultSlotMatcher shows whether browser, platform, extension, or version assumptions changed before a shared Grid receives traffic.

Verify Placement Through a Disposable Grid

Use a real Grid smoke workflow after unit tests. The numbered order matters because a successful session without inventory evidence can land on an unintended generic Node.

  1. Start a disposable Grid with one generic Chrome stereotype and one gpu Chrome stereotype.
  2. Query GraphQL and save Node IDs, statuses, and stereotypes before sending work.
  3. Request Chrome with qa:executionTier=gpu; require successful creation and record the owning Node ID.
  4. Request Chrome with an unsupported tier; require the configured fast rejection or bounded queue timeout.
  5. Request generic Chrome; prove whether it lands on the generic Node according to the reserved-lane rule.
  6. Send a malformed numeric tier through a low-level client; require rejection without Distributor instability.
  7. Quit every successful session and prove the queue returns to baseline.

Code 5: Java client request carrying the scheduling capability

Java
ChromeOptions options = new ChromeOptions();
options.setCapability("qa:executionTier", "gpu");
options.setCapability("se:name", "slot-matcher-positive-canary");

WebDriver driver = new RemoteWebDriver(
    URI.create("http://localhost:4444").toURL(), options);
try {
  driver.get("https://www.selenium.dev/");
  if (!driver.getTitle().contains("Selenium")) {
    throw new AssertionError("Unexpected canary title");
  }
} finally {
  driver.quit();
}

The GraphQL capacity dashboard article shows the documented Node and session fields needed for placement evidence. Compare the session's nodeId with the pre-run inventory. Do not infer placement from a browser capability echo because two Nodes can return the same browser details.

Inspect the queue between negative requests. A rejected capability, a waiting capability, and a matched request whose Node fails during startup have different owners. Use Session Map and queue inspection to preserve that distinction.

Roll Out Without Losing Capacity

Treat matcher deployment as a scheduler change. First deploy to a staging Grid with production-like stereotypes. Then canary one isolated pool, send shadow classification cases that do not start browsers where possible, and compare intended match results. Only after live positive and negative canaries pass should shared clients add the new capability.

Keep a rollback path that restores both the old matcher and compatible stereotypes. Rolling back only the JAR while Nodes still advertise reserved extension values can change which requests consume them. Rolling back only Node configuration can leave clients asking for a value no slot offers. Version those three pieces together: matcher artifact, Node stereotype manifest, and client capability contract.

During a Node rollout, preserve matching headroom for every required tier. Drain one Node, let owned sessions finish, replace it, confirm the expected stereotype, and run a tier canary before advancing. The Node draining guide provides the safe lifecycle. For short-lived workers, disposable Kubernetes Node patterns help keep image and stereotype identity aligned.

Monitor match outcomes indirectly through queue size, session creation errors, Node placement, and capacity by stereotype. Do not add high-cardinality logging inside every matches call in a busy Grid. A sampled diagnostic mode can record sanitized offered and requested tier values during canary, but routine telemetry should aggregate reason codes.

Define rollout stop conditions before canary traffic: any standard request newly rejected, any reserved request placed on a generic Node, queue residence beyond the agreed limit, Distributor error growth, or an unexplained drop in registered compatible capacity. Hold the canary long enough to cover both idle and burst demand. A five-request smoke proves class loading, but it cannot expose starvation caused by generic traffic consuming scarce tiers.

Load-test classification separately from browser startup. A pure benchmark can call the matcher across representative stereotypes and request candidates to detect allocation-path regressions without paying browser cost. Then run a smaller end-to-end load that validates queue and placement behavior. Keep both results because fast Boolean evaluation does not guarantee fair or sufficient capacity.

Migration tests should include clients that omit the capability. Older suites can be starved unexpectedly when every replacement Node becomes reserved. The Selenium capability migration guide is a useful inventory for legacy request shapes that may not use W3C names consistently.

Debug Loading, Matching, and Allocation Separately

If Grid fails during startup, inspect class loading first: extension JAR path, fully qualified class name, public constructor, Java version, Selenium Grid version, and missing transitive dependencies. A ClassNotFoundException is not a capability defect. Preserve the complete first exception and artifact hashes before changing configuration.

If Grid starts but every custom request is unsupported, query registered stereotypes. Confirm the namespaced key and JSON type reached the Grid Model exactly as intended. Then compare the request's alwaysMatch and firstMatch result with the matcher truth table. A string "true" and Boolean true are different values. So are a blank string and an absent capability.

If a request matches but lands on a surprising Node, remember the distinction between matching and selection. The matcher can return true for several slots; the slot selector then chooses one. Examine all eligible stereotypes, Node availability, remaining concurrency, and the recorded session owner. Trace the new-session request with a stable ID using the Grid trace-correlation workflow.

If the queue grows after rollout, classify requests by custom value and browser. Look for a missing tier, generic work barred from newly reserved Nodes, or a client typo. Compare compatible free capacity, not global free slots. A quick rollback may restore admission, but retain the failed request shape and Node inventory for a regression case.

Keep security and performance limits explicit. Matching code must not log secrets, trust client capabilities as identity, call external services, or allocate unbounded objects per comparison. Benchmark with realistic queue bursts and several stereotype shapes. The target is stable allocation behavior and actionable reason evidence, not a clever policy engine inside one Boolean method.

Frequently Asked Questions

Where does a custom Selenium Grid SlotMatcher run?

The Distributor uses the SlotMatcher when deciding whether a Node slot can support a new-session request. Standalone and Hub include a Distributor, while a fully distributed deployment runs it separately. Nodes advertise stereotypes, but installing the matcher only on a Node does not replace Distributor-side allocation policy.

Should a custom matcher replace DefaultSlotMatcher completely?

Usually no. Delegate to DefaultSlotMatcher first, then enforce the smallest additional namespaced capability rule. That preserves Selenium's browser, version, platform, and non-extension matching behavior. Reimplementing all standard matching creates upgrade risk and can route requests to slots that the browser driver cannot actually satisfy.

Can a custom capability be used for Grid authorization?

No. A client can place capability values in its own new-session request, so matching is not an authorization boundary. Use authenticated routing and platform identity for access control. Reserve custom capabilities for scheduling facts such as hardware class, network zone, licensed tool availability, or controlled browser image.

How should unsupported custom capabilities fail?

Reject malformed values deterministically and test whether an absent value means generic eligibility or reserved capacity. With --reject-unsupported-caps enabled in a static Grid, unsupported requests can fail quickly. Dynamic Grids may keep them queued while provisioning, so choose behavior from the deployment model and client timeout.

How do I verify that Grid loaded the custom matcher?

Capture startup logs, then send a positive request that only the intended stereotype can satisfy and a negative request that must remain unsupported or time out by policy. Query GraphQL for Node stereotypes and session placement. A class existing in the extension JAR is not proof that allocation invoked it.

What should be tested after a Selenium Grid upgrade?

Recompile the extension against the exact promoted Selenium Grid version, run unit truth-table tests, start a disposable Grid, inspect registered stereotypes, and execute positive, negative, absent, and malformed capability requests. Compare queue behavior and placement evidence before rolling the matcher into a shared production Grid.

Practice the Scheduler Contract

Start with one generic Chrome Node and one tiered Chrome Node. Write the truth table before code, then make each row observable through session placement or a bounded rejection. Add a browser mismatch to prove the custom rule did not weaken Selenium's defaults. Finally, drain the tiered Node and confirm that the request is unsupported rather than silently routed elsewhere.

Use QABattle's automation battles to practice capability and routing assertions under controlled failures. Pair the result with the Grid capacity dashboard so operators can see compatible supply, and retain trace correlation for allocation failures. A custom matcher is ready when its contract is smaller than its test evidence, every fallback is explicit, and rollback changes matcher, stereotype, and client behavior together.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 18, 2026 / Reviewed July 18, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Where does a custom Selenium Grid SlotMatcher run?

The Distributor uses the `SlotMatcher` when deciding whether a Node slot can support a new-session request. Standalone and Hub include a Distributor, while a fully distributed deployment runs it separately. Nodes advertise stereotypes, but installing the matcher only on a Node does not replace Distributor-side allocation policy.

Should a custom matcher replace DefaultSlotMatcher completely?

Usually no. Delegate to `DefaultSlotMatcher` first, then enforce the smallest additional namespaced capability rule. That preserves Selenium's browser, version, platform, and non-extension matching behavior. Reimplementing all standard matching creates upgrade risk and can route requests to slots that the browser driver cannot actually satisfy.

Can a custom capability be used for Grid authorization?

No. A client can place capability values in its own new-session request, so matching is not an authorization boundary. Use authenticated routing and platform identity for access control. Reserve custom capabilities for scheduling facts such as hardware class, network zone, licensed tool availability, or controlled browser image.

How should unsupported custom capabilities fail?

Reject malformed values deterministically and test whether an absent value means generic eligibility or reserved capacity. With `--reject-unsupported-caps` enabled in a static Grid, unsupported requests can fail quickly. Dynamic Grids may keep them queued while provisioning, so choose behavior from the deployment model and client timeout.

How do I verify that Grid loaded the custom matcher?

Capture startup logs, then send a positive request that only the intended stereotype can satisfy and a negative request that must remain unsupported or time out by policy. Query GraphQL for Node stereotypes and session placement. A class existing in the extension JAR is not proof that allocation invoked it.

What should be tested after a Selenium Grid upgrade?

Recompile the extension against the exact promoted Selenium Grid version, run unit truth-table tests, start a disposable Grid, inspect registered stereotypes, and execute positive, negative, absent, and malformed capability requests. Compare queue behavior and placement evidence before rolling the matcher into a shared production Grid.