“Playwright wasn’t working, can you test again.” It was working. It had been working an hour earlier in the same session. Nothing in the config had changed, no version had bumped, and the spec that “wasn’t working” was the same one that passed the last time we ran it. With an intermittent failure like that, the first honest answer is “we have no idea.” The reason we had no idea was sitting right next to it: a second clone.
We run several copies of the same SaaS codebase side by side. Each clone is a full working tree with its own branch, its own ticket, and its own Claude Code session driving it. Each of those sessions has a Playwright MCP server attached so the agent can drive a real Chromium against the local site, click through a flow, and screenshot the result. On a normal day three or four of these are open at once, because that is the entire point of running clones: parallel work on parallel tickets.
The browser turned out to have opinions about being run twice.
Pass, pass, pass, hang
From inside any one session the failures had no pattern. A navigate call would hang. A new-page call would time out. Sometimes the server came up fine and the first action wedged. Sometimes it never came up at all. Re-running the exact same step a minute later would often succeed, which is the textbook signature of a race. When the same input succeeds and fails on different runs with nothing else changed, the problem depends on timing.
The tell showed up across sessions. The Playwright “failures” clustered at the moments two clones were both trying to drive a browser. One session would be mid-flow, another would spin up its MCP server, and that was when the wedge happened. A failure that only appears when a neighbor is active points at something the sessions share.
One profile, many servers, one lock
Here is what they shared. The Playwright MCP config in .claude/mcp.json pinned the browser to a fixed profile directory:
"--user-data-dir", ".playwright-profile"A user-data-dir is Chromium’s profile folder: cookies, local storage, cache, and the session state that keeps a logged-in browser logged in. Pointing every server at the same directory sounds harmless and is even appealing, because every clone shares one warm, authenticated profile. Log in once, and every agent inherits the session.
The catch is that Chromium will not let two browser processes open the same profile directory at the same time. It enforces that with a lock file inside the profile (a SingletonLock on the profile dir, the same mechanism that makes a second chrome launch reattach to your existing window instead of starting a fresh one). A second process that tries to open a locked profile does not get a clean error you can catch and route around. It blocks and waits, and depending on timing it either wedges or gives up in a way that surfaces as a generic “the browser wouldn’t start.”
So with the shared --user-data-dir, every MCP server across every clone was contending for the same Chromium profile lock. One server held it. The others queued behind it or timed out trying to acquire it. The “intermittent Playwright failure” was a profile-lock deadlock, and it only looked intermittent because it depended on which two clones happened to reach for the browser at the same instant. Run one clone and it always works. Run four and they take turns failing.
The shared profile we thought we were getting away with was itself the bug.
One flag, one ephemeral profile per server
The Playwright MCP server takes an --isolated flag. We swapped the shared directory for it:
- "--user-data-dir", ".playwright-profile"+ "--isolated"In isolated mode the server spins up a fresh, ephemeral profile for its browser instead of opening a shared one on disk. Each MCP server now has a profile that no other server knows about, so there is no shared lock to contend for. Two clones launching browsers at the same moment are just two unrelated processes opening two different directories. Nothing to queue behind. Nothing to deadlock on.
That was the whole change. One flag, in one JSON file, and the intermittent failures stopped. We added no retry logic, no launch serialization, no semaphore around browser startup, and no “wait for the other clone to finish.” Removing the shared resource removed the contention outright, which beats coordinating access to a resource that never needed to be shared.
What isolation costs
--isolated is not free, and the cost is exactly what the shared profile was buying. An ephemeral profile does not persist between runs. Whatever you logged into in one session is gone the next time the server starts a browser. There is no warm authenticated state to inherit, because the whole point of the flag is that the profile is thrown away.
For our setup that cost is close to zero. Each clone authenticates fresh against its local site at the start of a flow anyway. The agent has the credentials, the login step is part of the run, and a clean profile every time is more correct: no stale cookie from a previous run changing behavior, and no leftover state that makes a test pass for the wrong reason. Isolated browsers are reproducible browsers.
If you were leaning on that shared profile to stay logged into something expensive (an OAuth dance, a multi-factor login, an account you do not want to re-authenticate against on every run), --isolated will hurt, and you will notice immediately because every run starts logged out. The flag forces the choice. Either each server authenticates itself on every run and you take isolation, or you need persisted auth and you have to give each parallel server its own dedicated profile directory. Pointing them all at one directory and running them at once is the configuration that deadlocks.
Before you run parallel browser sessions, audit every server config you are about to launch at the same time. Each one needs either --isolated or a distinct --user-data-dir. Any two that point at the same directory will queue behind the same Chromium lock, and the result will look like flakiness until you lay the configs side by side.
Related
- Our Agent’s Chrome Was Running but Invisible: A Windows Session 0 Isolation Ghost Story: another shared-resource isolation problem with browser automation
- Clearing %TEMP% on a machine running parallel Claude sessions is friendly fire: shared filesystem state causing cross-agent interference
- The CAPTCHA lockout you trigger yourself: when a wrong API endpoint hammers you to a 403: automation that triggers its own blocking through shared state
- The clone that stomped another clone’s lock, and the halt gate that should have existed: parallel agents contending for the same resource without a guard
- Every clone carried the same 12 stashes: spotting a filesystem-copy that duplicated the reflog: another multi-agent shared-state bug hiding in plain sight