# How chrome-devtools-mcp Handles Concurrent Tool Requests: The Mutex Implementation in src/Mutex.ts

> Learn how Chrome DevTools MCP serializes concurrent tool requests with its FIFO async Mutex in src/Mutex.ts, ensuring fair and deterministic ordering for DevTools state manipulation.

- Repository: [ChromeDevTools/chrome-devtools-mcp](https://github.com/chromedevtools/chrome-devtools-mcp)
- Tags: internals
- Published: 2026-02-16

---

**Chrome DevTools MCP serializes concurrent tool requests using a lightweight FIFO async Mutex implemented in [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts), ensuring that only one tool manipulates shared DevTools state at a time while maintaining fair, deterministic request ordering.**

The chrome-devtools-mcp repository provides a Model Context Protocol (MCP) server that exposes Chrome DevTools functionality to AI agents. Because multiple clients can invoke tools like **performance**, **emulation**, or **network** simultaneously, the server must prevent race conditions when accessing shared Puppeteer `Browser` instances or DevTools protocol sessions.

## Why Concurrent Tool Requests Require Serialization

When MCP clients send simultaneous tool requests, parallel execution risks corrupting shared state. Two concurrent calls modifying browser emulation settings could interleave operations, leaving the `Browser` instance in an inconsistent state. To eliminate this risk, chrome-devtools-mcp implements a **global serialization mechanism** that forces all tool handlers to execute sequentially in the exact order they were received.

## Global Tool Serialization with toolMutex in src/main.ts

The entry point [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts) instantiates a single global mutex and wraps every registered tool handler with exclusive acquisition logic.

At the top of the file, the server creates the mutex instance:

```typescript
const toolMutex = new Mutex();

```

When registering tool handlers, the code wraps the execution flow with an acquisition pattern:

```typescript
const guard = await toolMutex.acquire();
try {
  // ... tool logic that accesses shared DevTools state ...
  return await executeToolLogic(params);
} finally {
  guard.dispose();
}

```

This pattern guarantees that **only one tool request holds the lock at any moment**. The `acquire()` method returns a `Guard` object that must be disposed to release the lock, ensuring cleanup occurs even if the tool throws an exception.

## Internal Universe Management with Mutex in src/DevtoolsUtils.ts

Beyond tool handlers, the `UniverseManager` class in [`src/DevtoolsUtils.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/DevtoolsUtils.ts) uses a dedicated `Mutex` to synchronize the creation and deletion of DevTools "universes"—isolated DevTools sessions bound to Puppeteer `Page` instances.

The mutex ensures that:
- Universe initialization never overlaps with cleanup
- No duplicate universes are created for the same page
- Stale references are safely removed without race conditions

This internal usage demonstrates that the `Mutex` primitive is reusable across different subsystems requiring deterministic, sequential access to shared asynchronous resources.

## The FIFO Async Mutex Implementation in src/Mutex.ts

The [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts) file implements a lightweight, zero-dependency **FIFO (first-in-first-out) async mutex** in pure TypeScript. The design prioritizes fairness: requests are served in the exact order they were received, preventing starvation.

### The Guard Class

The `Guard` class acts as a RAII (Resource Acquisition Is Initialization) handle for the lock:

```typescript
export class Guard {
  #mutex: Mutex;
  
  constructor(mutex: Mutex) {
    this.#mutex = mutex;
  }
  
  dispose(): void {
    this.#mutex.release();
  }
}

```

When `dispose()` is called, it invokes the parent mutex's `release()` method, ensuring the lock is freed even if the protected code throws an error.

### Private State Fields

The mutex maintains two private fields:

```typescript
#locked: boolean = false;
#acquirers: Array<(guard: Guard) => void> = [];

```

- `#locked` tracks whether the mutex is currently held
- `#acquirers` stores a queue of resolver functions for pending lock requests

### The acquire() Method

The `acquire()` method implements the FIFO queuing logic:

```typescript
async acquire(): Promise<Guard> {
  if (!this.#locked) {
    this.#locked = true;
    return new Guard(this);
  }
  
  return new Promise<Guard>((resolve) => {
    this.#acquirers.push(resolve);
  });
}

```

If the lock is free, it is taken immediately. Otherwise, the caller is suspended in a `Promise` that resolves when all prior queued requests have completed and released the lock.

### The release() Method

The `release()` method handles handoff to the next waiter:

```typescript
release(): void {
  const next = this.#acquirers.shift();
  if (next) {
    next(new Guard(this));
  } else {
    this.#locked = false;
  }
}

```

By shifting from the front of the `#acquirers` array, the mutex guarantees **strict FIFO ordering**. If the queue is empty, the mutex simply unlocks.

## Practical Usage Examples

### Serializing Tool Invocations

The primary use case in chrome-devtools-mcp wraps every tool handler to prevent concurrent access to the DevTools protocol:

```typescript
import {Mutex} from './Mutex.js';

const toolMutex = new Mutex();

async function handlePerformanceTool(params) {
  const guard = await toolMutex.acquire();
  try {
    // Exclusive access to Chrome DevTools protocol
    const metrics = await page.metrics();
    return {result: metrics};
  } finally {
    guard.dispose();
  }
}

```

### Manual Critical Section Protection

You can reuse the same primitive for any asynchronous critical section:

```typescript
import {Mutex} from './src/Mutex.js';

const mutex = new Mutex();

async function criticalTask(id: number) {
  const guard = await mutex.acquire();
  try {
    console.log(`Task ${id} executing`);
    await someAsyncWork();
  } finally {
    guard.dispose();
    console.log(`Task ${id} completed`);
  }
}

// Launch several overlapping calls – they will run one after another
criticalTask(1);
criticalTask(2);
criticalTask(3);

```

Output will always show tasks completing in order **1 → 2 → 3**, demonstrating the FIFO guarantee.

## Summary

- chrome-devtools-mcp handles **concurrent tool requests** by serializing execution through a global `toolMutex` instance defined in [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts).
- The **Mutex implementation** in [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts) provides a lightweight, zero-dependency **FIFO async lock** using a private queue of Promise resolver functions.
- Every tool handler wraps its execution in `await toolMutex.acquire()` and `guard.dispose()`, ensuring **exclusive access** to shared DevTools resources.
- The same primitive protects **UniverseManager** operations in [`src/DevtoolsUtils.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/DevtoolsUtils.ts), preventing race conditions during DevTools session creation and cleanup.
- The **Guard-based RAII pattern** ensures locks are released even when exceptions occur, preventing deadlocks.

## Frequently Asked Questions

### How does chrome-devtools-mcp prevent race conditions between simultaneous tool calls?

Chrome-devtools-mcp prevents race conditions by wrapping every tool handler with a global **FIFO mutex** (`toolMutex`) instantiated in [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts). When a client invokes a tool, the handler must first `await toolMutex.acquire()`, which queues the request if another tool is currently running. This guarantees that only one tool accesses the shared Puppeteer `Browser` or DevTools protocol at a time, executing requests in the exact order they were received.

### What makes the Mutex implementation in src/Mutex.ts different from other JavaScript locks?

The [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts) implementation is specifically designed for **fair FIFO ordering** and **zero dependencies**. Unlike simple boolean locks or semaphore libraries that may resolve waiters in unpredictable order, this mutex maintains a private `#acquirers` queue (an array of Promise resolvers) and always shifts from the front in the `release()` method. This ensures strict first-in-first-out execution, preventing starvation and making tool behavior deterministic under high concurrency.

### Can the Mutex be used outside of the chrome-devtools-mcp server for other async operations?

Yes, the `Mutex` class is a generic, reusable primitive that can protect any asynchronous critical section in Node.js or browser TypeScript code. Because it has no external dependencies and uses standard JavaScript private fields and Promises, you can import it into any project requiring serialized access to shared resources. The `Guard` pattern with `dispose()` makes it particularly safe for use with `try/finally` blocks, ensuring locks release even if the protected code throws exceptions.

### How does the Guard pattern in the Mutex implementation prevent deadlocks?

The `Guard` class implements a **RAII (Resource Acquisition Is Initialization)** pattern where the lock holder receives a `Guard` instance from `acquire()`. When the `dispose()` method is called—typically in a `finally` block—it automatically invokes the parent mutex's `release()` method. This design ensures that the lock is always released when the guard goes out of scope or is explicitly disposed, even if the protected asynchronous code throws an error or returns early, effectively eliminating the risk of deadlocks from forgotten lock releases.