# How Chrome DevTools MCP Handles Concurrent Tool Calls Using Mutex

> Learn how Chrome DevTools MCP uses a global FIFO mutex to serialize concurrent tool calls, preventing race conditions and ensuring safe access to shared browser state.

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

---

**Chrome DevTools MCP serializes concurrent tool invocations using a global FIFO mutex to prevent race conditions when accessing shared browser state.**

The ChromeDevTools/chrome-devtools-mcp repository implements a strict concurrency control mechanism that ensures only one MCP tool manipulates the DevTools protocol or browser state at any given time. By wrapping every tool handler with an asynchronous mutex, the server prevents data races while maintaining deterministic execution order.

## Why Concurrent Tool Calls Need Serialization in MCP

MCP (Model Context Protocol) servers can receive multiple simultaneous requests from different clients or parallel tool invocations. When these requests target the same Puppeteer `Browser` instance or DevTools session, parallel execution creates **race conditions**—for example, one call might navigate the page while another attempts to capture a screenshot, resulting in inconsistent or failed operations.

The Chrome DevTools MCP solves this by treating the entire tool execution pipeline as a **critical section** that requires exclusive access.

## The Global toolMutex Pattern in src/main.ts

In [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts), the server instantiates a single global mutex at module initialization:

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

```

Every registered tool handler wraps its execution logic with this mutex using a consistent pattern:

```typescript
const guard = await toolMutex.acquire();
try {
  // ... tool execution logic ...
  return await performToolOperation(params);
} finally {
  guard.dispose();
}

```

This implementation guarantees **FIFO (First-In-First-Out)** ordering. Requests are processed strictly in the order they arrived, preventing starvation and ensuring predictable behavior under load.

## Inside the Mutex Implementation (src/Mutex.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 asynchronous locking primitive.

### FIFO Queue and Fair Ordering

The mutex maintains two private fields:

- `#locked`: Boolean indicating if the mutex is currently held
- `#acquirers`: Array of resolver functions representing queued requests

When `acquire()` is called on a locked mutex, it creates a new Promise and pushes the resolver onto `#acquirers`. The FIFO queue ensures that when `release()` is called, it resolves the oldest waiting request first, maintaining fair ordering.

### The Guard Class and RAII Pattern

The `Guard` class implements the **RAII (Resource Acquisition Is Initialization)** pattern:

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

```

This design ensures that the lock is always released when the guard is disposed, even if the tool handler throws an exception. The `finally` block in [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts) guarantees cleanup without requiring explicit error handling in every tool implementation.

## Protecting DevTools Universe Creation

Beyond tool execution, the mutex pattern appears in [`src/DevtoolsUtils.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/DevtoolsUtils.ts) within the `UniverseManager` class. This manager handles the creation and deletion of DevTools "universes"—isolated DevTools protocol sessions tied to specific Puppeteer pages.

Using a dedicated mutex for universe lifecycle operations prevents:

- Duplicate universe creation for the same page
- Race conditions between universe initialization and cleanup
- Stale references when pages close while tools are executing

## Practical Usage Examples

### Basic Tool Handler Pattern

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

const mutex = new Mutex();

async function safeToolCall(params: any) {
  const guard = await mutex.acquire();
  try {
    console.log('Executing tool with exclusive access');
    await performOperation(params);
    return { success: true };
  } finally {
    guard.dispose();
    console.log('Lock released');
  }
}

```

### Concurrent Request Simulation

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

async function concurrentTask(id: number) {
  const guard = await toolMutex.acquire();
  try {
    console.log(`Task ${id} started`);
    await new Promise(resolve => setTimeout(resolve, 100));
    console.log(`Task ${id} completed`);
  } finally {
    guard.dispose();
  }
}

// Launch three overlapping tasks
Promise.all([
  concurrentTask(1),
  concurrentTask(2),
  concurrentTask(3)
]);

```

Output will always show sequential execution: Task 1 starts and completes, then Task 2, then Task 3, demonstrating the FIFO guarantee.

## Summary

- Chrome DevTools MCP uses a **global `toolMutex`** instance in [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts) to serialize all tool invocations
- The **FIFO mutex implementation** in [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts) guarantees fair ordering and prevents starvation using a queue of resolver functions
- **RAII-style Guard objects** ensure locks are always released via `dispose()`, even when exceptions occur
- The same pattern protects **DevTools universe lifecycle** operations in [`src/DevtoolsUtils.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/DevtoolsUtils.ts) to prevent race conditions during creation and cleanup
- This architecture allows the Node.js event loop to remain responsive while ensuring **exclusive access** to shared browser state

## Frequently Asked Questions

### What happens if a tool call throws an exception while holding the mutex?

The `finally` block in [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts) guarantees that `guard.dispose()` runs regardless of whether the tool handler succeeds or throws. This releases the mutex immediately, preventing deadlocks and allowing the next queued request to proceed.

### Why does Chrome DevTools MCP use a FIFO mutex instead of a simple boolean lock?

A simple boolean lock could lead to **starvation** or unpredictable ordering under high concurrency. The FIFO queue in [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts) ensures that tool requests are processed in the exact order they arrived, providing deterministic behavior that matches client expectations and simplifies debugging.

### Can multiple different tools run concurrently if they target different browser pages?

No. The current implementation uses a **single global mutex** (`toolMutex` in [`src/main.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/main.ts)) that serializes all tool executions regardless of their target page or tool type. While this is conservative, it prevents any potential race conditions in shared DevTools protocol state or Puppeteer internals. Future versions could implement finer-grained locking per page or domain if profiling reveals contention bottlenecks.

### How does the Mutex implementation differ from standard JavaScript Promise patterns?

Unlike basic Promise-based locks that might use `Promise.resolve()` chains, the `Mutex` class in [`src/Mutex.ts`](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/main/src/Mutex.ts) maintains an explicit **array of resolver functions** (`#acquirers`). This design choice enables true FIFO ordering and provides the `Guard` abstraction for automatic cleanup, distinguishing it from simpler semaphore implementations or the `async-mutex` library patterns.