all posts

The best Selenium Grid alternatives in 2026

Ajay Kumar··10 min read

Selenium Grid is a good piece of software solving a genuinely hard problem, and most teams that leave it are not leaving because of the software. They are leaving because of a property of the architecture: a Grid node is a long-lived process running a long-lived browser, and browsers accumulate state — profile directories, service workers, cached credentials, orphaned renderer processes, a `/tmp` slowly filling with crash dumps.

Once state accumulates, a test failure has two possible causes and you cannot tell them apart. Either the application is broken, or the node is. And the standard remedy — retry the test, and if it passes twice call it flaky — is how a suite stops being trusted. This guide is about the alternatives, organised by what they actually change about that problem.

No prices, session limits, or concurrency tiers for third-party products below. Browser-testing vendors price on parallel sessions and minutes, and those change often enough that any figure here would mislead. Check current docs before committing a suite.

What actually hurts about running a Grid

Nodes accumulate state, and state causes flake

A browser session is supposed to be clean. In practice, WebDriver's cleanup is best-effort: a crashed browser leaves a profile directory behind, a hung renderer keeps a lock file, a test that authenticated leaves a cookie jar. The next session on that node inherits some of it. The result is order-dependent failures — the test that fails only when it runs fourth — which are the most expensive kind to debug because the reproduction requires the same sequence on the same node.

The `/dev/shm` problem, which everyone hits exactly once

Chrome uses shared memory heavily and Docker's default `/dev/shm` is 64 MB, which is not enough. The failure is spectacular and unhelpful: tabs crash, the driver reports a lost connection, and the test output blames your application. The standard workarounds are `--disable-dev-shm-usage`, which trades speed for stability, or mounting a larger shm. Any alternative you evaluate should either give the browser real memory or have solved this on your behalf — and it is a fair interview question for a vendor.

# The two lines that fix most self-hosted Grid instability. If you are
# staying on Grid, do these before you evaluate anything else --
# they resolve a surprising share of "flaky" suites.
services:
  chrome-node:
    image: selenium/node-chromium:latest
    shm_size: 2gb            # NOT the 64MB default. Chrome needs real shm.
    environment:
      # One session per node, then recycle the container. This is the
      # single biggest flake reduction available on Grid: a node that
      # never serves a second session cannot leak state into one.
      - SE_NODE_MAX_SESSIONS=1
      - SE_NODE_OVERRIDE_MAX_SESSIONS=false
      - SE_DRAIN_AFTER_SESSION_COUNT=1
      # Fail fast rather than hanging a CI job for an hour.
      - SE_NODE_SESSION_TIMEOUT=300

Capacity is a lie you tell yourself in YAML

You configure a node to accept four sessions because the machine has four cores. Then a test opens a page with a heavy JavaScript bundle, Chrome spawns a renderer per tab plus a GPU process, memory doubles, and the fourth session gets a browser that is technically alive and hopelessly slow. Grid's scheduler knows about session slots; it does not know about your actual resource pressure. The symptom is timeouts that correlate with suite parallelism rather than with any code change.

It is infrastructure you own, forever

Browser versions move monthly, driver versions must match, the hub is a single point of coordination, and someone has to care. None of this is hard; all of it is continuous. For many teams the honest reason to move is that nobody wants to own it, which is a completely legitimate reason to buy something.

Option 1: keep WebDriver, change who runs it

The lowest-risk migration keeps your test code exactly as it is. Selenium speaks the W3C WebDriver protocol over HTTP, so anything that terminates that protocol can replace your hub — you change the remote URL and the capabilities block, and nothing else.

  • BrowserStack Automate — The broadest real-device and real-browser matrix, including older Safari and IE-mode cases nothing else covers. The reason to pick it is coverage you cannot self-host, not convenience.
  • Sauce Labs — Long-standing managed Grid with strong enterprise integration, analytics across runs, and good debugging artefacts. Similar shape to BrowserStack; evaluate both on the matrix you actually need.
  • LambdaTest — Managed grid with a large browser matrix and a focus on parallel throughput. Frequently the pragmatic middle option.
  • Selenium Grid on Kubernetes, with KEDA — Still Grid, but with nodes as short-lived pods created per session and destroyed after. This fixes the state-leak problem without changing your tests at all, and it is genuinely the right answer for a lot of teams. You still own the browser-version treadmill.
  • Zalenium's successors and one-shot node patterns — Various community approaches to the same idea: one session per container, then throw it away.

Option 2: change the automation library

The other direction is to stop using WebDriver. Playwright and Puppeteer drive browsers over DevTools-style protocols with a persistent connection, which changes the ergonomics substantially: auto-waiting instead of explicit sleeps, network interception as a first-class feature, browser contexts that are cheap and genuinely isolated, and tracing that records a replayable timeline of a failure.

The relevant detail for this discussion is browser contexts. A Playwright context is an isolated profile inside one browser process — separate cookies, storage, and cache — created in milliseconds. That means a lot of the isolation you were trying to get from fresh nodes, you get inside a single browser, cheaply. It does not remove the need for a clean machine, but it moves the boundary to somewhere much less expensive.

The cost is a migration. Playwright's API is not WebDriver's, and a large Selenium suite is real work to port. Two mitigations worth knowing: Selenium 4 and later can speak BiDi, which brings some of the same capabilities to your existing code, and a hybrid estate is fine — port the flakiest specs first and leave the stable ones alone.

Option 3: managed browser infrastructure

A newer category runs browsers as a service for automation and agent workloads rather than specifically for test grids: you connect over CDP or WebDriver to a browser somebody else operates, with session recording, proxy egress, and captcha or fingerprint handling as product features.

  • Browserbase — Managed headless browsers aimed at automation and AI agent use, with session inspection and a proxy layer. Strong when browsing is part of your product rather than only part of your test suite.
  • Browserless — Hosted or self-hosted Chrome over CDP, with a long track record and a straightforward self-host story if you want the same interface on your own hardware.
  • Steel and similar agent-oriented browser services — Session-centric APIs designed for long-running agent browsing, with state you can pause and resume.
  • Cloud provider browser services — Several clouds now offer managed browser endpoints for automation; convenient if your egress and IAM story already lives there.

Option 4: one disposable machine per session

The strongest version of the fix is to make the unit of isolation a whole machine rather than a container or a session, and to make that machine disposable. Every root cause in the first section — leaked profile state, shared `/dev/shm`, invisible resource pressure, cross-session interference — is a consequence of sharing something. Stop sharing it and the class of failure goes away rather than getting mitigated.

This used to be economically silly, because a VM per test session meant minutes of boot time. Snapshot-restore changes the arithmetic: PandaStack boots a microVM from a memory snapshot in roughly 180 milliseconds, and its `browser` template ships Chromium with Playwright already installed, 4 GiB of RAM, and 8 vCPUs of burst capacity. A test shard gets its own kernel, its own page cache, its own `/dev/shm`, and its own network namespace, then ceases to exist.

// One microVM per test shard. The isolation boundary is a kernel, so
// there is no shared /dev/shm, no shared /tmp, and no possibility that
// shard 3 inherits shard 2's cookie jar.
import { Sandbox } from "@pandastack/sdk";

const SHARDS = 8;
const REPO = "https://github.com/your-org/your-app.git";

async function runShard(index: number) {
  const sbx = await Sandbox.create({
    template: "browser",                 // Chromium + Playwright preinstalled
    metadata: { shard: String(index) },
  });
  try {
    await sbx.filesystem.write("/work/run.sh", [
      "set -euo pipefail",
      `git clone --depth 1 ${REPO} /work/repo`,
      "cd /work/repo",
      "npm ci --prefer-offline",
      // Playwright's own sharding: one shard per VM. The browsers are
      // already in the image, so skip the browser download step.
      `npx playwright test --shard=${index + 1}/${SHARDS} --reporter=json`,
    ].join("\n"));

    const res = await sbx.commands.run("bash /work/run.sh", {
      timeoutSeconds: 1800,
    });

    // Pull the artifacts out BEFORE you destroy the machine -- traces are
    // the whole reason a failing browser test is debuggable at all.
    if (res.exitCode !== 0) {
      await sbx.commands.run(
        "tar czf /tmp/trace.tgz -C /work/repo test-results",
      );
      await sbx.filesystem.download(
        "/tmp/trace.tgz",
        `./traces/shard-${index}.tgz`,
      );
    }
    return { index, ok: res.exitCode === 0 };
  } finally {
    await sbx.kill();
  }
}

const results = await Promise.all(
  Array.from({ length: SHARDS }, (_, i) => runShard(i)),
);
const failed = results.filter((r) => !r.ok);
if (failed.length) {
  console.error(`shards failed: ${failed.map((f) => f.index).join(", ")}`);
  process.exit(1);
}}
Be honest with yourself about what a disposable machine does and does not fix. It removes cross-session contamination and resource contention completely. It does nothing about the other big source of browser-test flake: your own tests racing against your own application — waiting on a selector that appears before it is interactive, asserting on an animation mid-transition, depending on a shared test account another shard is also mutating. Fresh machines make those failures reproducible, which is progress, but they do not make them go away.

Pick by situation

  • You need real Safari, real iOS, or an old Windows browser → BrowserStack or Sauce Labs. This is coverage you cannot self-host, and it is the clearest case for buying.
  • Your suite is fine but the nodes are flaky, and you cannot rewrite tests → Grid on Kubernetes with one session per pod, or a managed grid. Same protocol, same tests, the state problem gone.
  • You are writing new browser tests in 2026 → Playwright. Auto-waiting, tracing, and cheap contexts remove whole categories of flake that Grid users treat as unavoidable.
  • Browser automation is part of your product, not just your tests → managed browser infrastructure like Browserbase or Browserless, or your own microVM fleet if egress control and isolation matter more than convenience.
  • You need the suite to finish in five minutes instead of fifty → sharding across disposable machines, whichever substrate. Parallelism on shared nodes is where the resource-pressure timeouts come from.
  • You are testing untrusted content — user-submitted URLs, scraped pages, agent-driven browsing → a VM boundary, not a container one. A browser rendering hostile input is the classic case for a hypervisor between it and your host.
  • You have a working Grid and nobody is complaining → keep it. Set shm_size and one-session-per-node and go do something else.

The short version

Before migrating anything, spend an afternoon on two settings: give Chrome a real `/dev/shm` and make every node serve exactly one session before being recycled. That combination fixes a surprising share of what people call flakiness, and it tells you whether your remaining failures are infrastructural or your own.

If you still want to move, the decision splits cleanly. Buy a managed grid when you need browser coverage you cannot host — real Safari, real devices, old versions. Move to Playwright when you are willing to invest in the test code and want the flake removed at the source. Run disposable machines when isolation and throughput are the constraint and you would rather own the substrate. All three are better than adding another retry.

Frequently asked questions

Is Selenium Grid deprecated?

No. Selenium Grid is actively maintained, Selenium 4 modernised it substantially, and the W3C WebDriver protocol it speaks is a standard rather than a legacy interface. What has changed is that the ecosystem around it moved: Playwright and Puppeteer offer a different automation model with auto-waiting and cheap browser contexts, and orchestration platforms make one-session-per-container patterns easy. Teams move off Grid for operational reasons — accumulating node state, browser-version maintenance, capacity that is configured rather than measured — not because the project is going away. If your Grid is stable and someone is happy to own it, staying is a defensible engineering decision.

Why are my Selenium tests flaky on Grid but not locally?

Three causes, in rough order of frequency. First, shared /dev/shm: Docker defaults it to 64 MB and Chrome needs far more, so tabs crash under load and the driver reports a lost connection that looks like your application failing. Set shm_size to 2gb or pass --disable-dev-shm-usage. Second, state left behind by previous sessions on the same node — profile directories, cookies, lock files from a crashed browser — which produces order-dependent failures that only appear at certain parallelism. Set the node to serve one session and then drain. Third, resource pressure Grid cannot see: session slots are configured by hand, so four concurrent heavy pages on a four-core node produce browsers that are alive and far too slow, and the timeouts correlate with suite parallelism rather than with any code change. Fix those three before rewriting a single test.

Should I migrate from Selenium to Playwright?

For new tests, yes, almost without qualification: auto-waiting removes the explicit-sleep habit that causes most Selenium flake, tracing gives you a replayable timeline of a failure instead of a screenshot and a stack trace, and browser contexts give you real isolation for the cost of a few milliseconds. For an existing large suite, migrate incrementally rather than in one project. Port the flakiest and most-changed specs first, where the payback is immediate, and leave the stable ones on Selenium indefinitely — a mixed estate is normal and fine. Also worth knowing before you commit to a rewrite: Selenium 4's BiDi support brings some of Playwright's capabilities to your existing code, which may resolve your specific complaint without a migration at all.

What is the cheapest way to run browser tests in parallel?

Cheapest usually means self-hosted on ephemeral compute, because managed grids price per parallel session and browser tests are bursty — you want fifty sessions for six minutes a day and zero for the rest. The pattern that costs least is disposable workers created per run and destroyed afterwards, with the test framework's own sharding splitting the suite. The costs that surprise people are not compute: they are the artifacts. Traces, videos, and screenshots are large, and storing them for every run of every branch adds up faster than the machines do — so capture them on failure only, and set a retention policy. Second surprise is egress if your tests load third-party resources from a cloud that bills for it. Compare on total run cost, not per-session price.

Do I need one browser per test or can they share?

Share a browser, isolate the context. Launching a browser process is expensive — hundreds of milliseconds and a lot of memory — while creating an isolated context inside a running browser is close to free and gives you separate cookies, local storage, and cache. So the efficient pattern is one browser per worker process and one fresh context per test, which is exactly what Playwright's test runner does by default. Where you do want a harder boundary is between workers and between untrusted workloads: a crashed or compromised browser takes its whole process down, and a browser rendering content you do not control should have a kernel between it and anything you care about. Practically: context per test, process per worker, VM per untrusted workload.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.