“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 I ran it. That kind of intermittent failure is the worst kind, because the honest first answer is “I have no idea.” The reason I had no idea was sitting right next to it: a second clone.

I 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.

It turns out the browser has opinions about being run twice.

The symptom: pass, pass, pass, hang

The failures had no pattern I could see from inside one session. 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 rather than a logic bug. When the same input produces success and failure on different runs with nothing else changed, you are fighting something timing-dependent, not something wrong in your code.

The tell was correlation across sessions, not within one. The Playwright “failures” clustered at the moments two clones were both trying to drive a browser. One session would be mid-flow and 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, not at either session.

The root cause: 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 makes a logged-in browser stay logged in. Pointing every server at the same directory sounds harmless and is even appealing, because it means every clone shares one warm, authenticated profile. Log in once, and every agent inherits the session.

The problem 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). When a second process tries to open a profile that is already locked, it does not get a clean error you can catch and route around. It blocks, waits, and depending on timing either eventually 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 exact 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 was intermittent only because it depended on which two clones happened to reach for the browser at the same instant. Run one clone, it always works. Run four, and they take turns failing.

The shared profile I thought I was getting away with was itself the bug.

The fix: give each server its own ephemeral profile

The Playwright MCP server takes an --isolated flag. Swap 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. No retry logic, no launch serialization, no semaphore around browser startup, no “wait for the other clone to finish.” Removing the shared resource removed the contention outright, which is a much better fix than coordinating access to a resource you did not need to share in the first place.

The trade-off you are actually accepting

--isolated is not free, and the cost is exactly the thing the shared profile was buying you. 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 entire point of the flag is that the profile is thrown away.

For my setup that cost is close to zero, because 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 silently changing behavior, no leftover state that makes a test pass for the wrong reason. Isolated browsers are reproducible browsers.

But 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) then --isolated will hurt, and you will notice immediately because every run starts logged out. That is the decision the flag forces you to make explicitly. Either each server authenticates itself on every run and you take isolation, or you genuinely need persisted auth and you have to give each parallel server its own dedicated, non-shared profile directory instead. What you cannot do is point them all at one directory and run them at once. That is the configuration that deadlocks.

Before you run parallel browser sessions, audit every server config you’re about to launch at the same time. Each one needs either --isolated or a distinct, non-shared --user-data-dir. Any two that point at the same directory will queue behind the same Chromium lock, and that failure will look exactly like flakiness until you check the configs side by side.