# How Hyperframes Manages a Chrome Browser Pool for High-Throughput Rendering

> Discover how Hyperframes efficiently manages a Chrome browser pool for high-throughput rendering. Learn about reference counting, capture-mode validation, and promise deduplication to cut browser process overhead.

- Repository: [HeyGen/hyperframes](https://github.com/heygen-com/hyperframes)
- Tags: how-to-guide
- Published: 2026-05-17

---

**Hyperframes maintains a single shared Chrome instance across parallel rendering workers using reference counting, capture-mode validation, and promise deduplication to eliminate the overhead of launching separate browser processes per task.**

The Hyperframes video rendering engine drives headless Chrome via Puppeteer to generate frames from React compositions. When operating at scale—such as in render farms or CI pipelines spawning dozens of workers—launching a distinct Chrome binary per worker would overwhelm system resources. To solve this, the engine implements a lightweight **browser pool** in [`packages/engine/src/services/browserManager.ts`](https://github.com/heygen-com/hyperframes/blob/main/packages/engine/src/services/browserManager.ts) that reuses one Chrome instance while ensuring thread safety and mode compatibility.

## Global Pool State and Configuration

The pool relies on module-level variables that persist for the duration of the Node.js process. As defined at the top of [`browserManager.ts`](https://github.com/heygen-com/hyperframes/blob/main/browserManager.ts), these track the shared instance, its current usage, and the active capture mode:

- `pooledBrowser`: Stores the single `Browser` instance shared across workers.
- `pooledBrowserRefCount`: Tracks how many workers currently hold the pooled browser.
- `pooledCaptureMode`: Records whether the pooled instance uses **Begin-frame** or **screenshot** mode to ensure compatibility.
- `_pooledBrowserLaunchPromise`: Caches the launch promise to deduplicate concurrent acquisition requests.
- `_autoBrowserGpuModeCache`: Caches GPU capability probes to avoid repeated detection.

Pooling is controlled by the `enableBrowserPool` configuration option, which defaults to `true` via `DEFAULT_CONFIG.enableBrowserPool` in [`packages/engine/src/config.ts`](https://github.com/heygen-com/hyperframes/blob/main/packages/engine/src/config.ts).

## Acquire Logic and Mode Validation

Workers request a browser via `acquireBrowser(chromeArgs, config)`, which implements a multi-step reuse strategy:

1. **Pool enabled check**: The function first verifies `config.enableBrowserPool` (defaulting to `true`).
2. **Existing instance validation**: If `pooledBrowser` exists and `.connected` is true, the function compares the requested capture mode (via `resolveRequestedCaptureMode(config)`) against `pooledCaptureMode`.
3. **Reference increment**: When modes match, `pooledBrowserRefCount` increments and the existing `Browser` object returns immediately, avoiding any launch overhead.

If the requested mode differs from the pooled instance—for example, requesting screenshot mode when the pool holds a Begin-frame-only browser—the system does **not** evict the pool. Instead, it launches a **dedicated** Chrome instance for that specific caller. This prevents deadlocks that would occur if a screenshot task received a browser optimized only for Begin-frame capture.

## Deduplicating Concurrent Launches

Under high contention, multiple workers may call `acquireBrowser()` simultaneously before the first Chrome instance finishes launching. To prevent redundant browser startups, the implementation uses promise caching:

When no pooled browser exists and multiple requests arrive, the first caller triggers `launchBrowser(chromeArgs, config)` and stores the resulting promise in `_pooledBrowserLaunchPromise`. Subsequent callers await this same promise rather than initiating their own launch. Once the promise resolves, the resolved `Browser` and its `captureMode` populate the pool variables, the ref-count initializes to `1`, and the launch promise clears.

`launchBrowser()` itself, defined in the same file, selects the appropriate Chrome binary (`chrome-headless-shell` on Linux for Begin-frame mode) and constructs flags via [`packages/engine/src/services/buildChromeArgs.ts`](https://github.com/heygen-com/hyperframes/blob/main/packages/engine/src/services/buildChromeArgs.ts).

## Releasing and Draining the Pool

When a worker finishes rendering, it calls `releaseBrowser(browser, config)`:

- **Non-pooled browsers**: Close immediately.
- **Pooled browsers**: Decrement `pooledBrowserRefCount`. When the count reaches `0`, the instance closes and pool variables reset to `null`.

For graceful shutdowns or between independent render jobs, the `drainBrowserPool()` function ensures clean cleanup:

1. Awaits any pending `_pooledBrowserLaunchPromise` and closes the resulting browser if it resolved.
2. Closes the active `pooledBrowser` instance and clears global state.

This prevents zombie Chrome processes from lingering after the Node.js process exits.

## Implementation Examples

Below are the core workflows for acquiring and releasing browsers from the pool:

**Acquiring a browser with automatic pool reuse:**

```typescript
// packages/engine/src/services/browserManager.ts
export async function acquireBrowser(
  chromeArgs: string[],
  config?: Partial<EngineConfig>,
): Promise<AcquiredBrowser> {
  const enablePool = config?.enableBrowserPool ?? DEFAULT_CONFIG.enableBrowserPool;

  // Return existing pooled browser if mode matches
  if (enablePool && pooledBrowser) {
    if (!pooledBrowser.connected) {
      // Reset stale pool logic omitted
    } else {
      const requested = resolveRequestedCaptureMode(config);
      if (pooledCaptureMode === requested) {
        pooledBrowserRefCount += 1;
        return { browser: pooledBrowser, captureMode: pooledCaptureMode };
      }
    }
  }

  // Deduplicate concurrent launches
  if (enablePool && _pooledBrowserLaunchPromise) {
    const result = await _pooledBrowserLaunchPromise;
    return result;
  }

  // Launch new browser and populate pool
  const launchPromise = launchBrowser(chromeArgs, config);
  if (enablePool && !pooledBrowser) {
    _pooledBrowserLaunchPromise = launchPromise;
    const result = await launchPromise;
    pooledBrowser = result.browser;
    pooledCaptureMode = result.captureMode;
    pooledBrowserRefCount = 1;
    _pooledBrowserLaunchPromise = null;
    return result;
  }
  
  return launchPromise;
}

```

**Releasing a browser back to the pool:**

```typescript
// packages/engine/src/services/browserManager.ts
export async function releaseBrowser(
  browser: Browser,
  config?: Partial<EngineConfig>,
): Promise<void> {
  const enablePool = config?.enableBrowserPool ?? DEFAULT_CONFIG.enableBrowserPool;
  
  if (!enablePool) {
    await browser.close().catch(() => {});
    return;
  }

  if (pooledBrowser && pooledBrowser === browser) {
    pooledBrowserRefCount = Math.max(0, pooledBrowserRefCount - 1);
    if (pooledBrowserRefCount === 0) {
      await browser.close().catch(() => {});
      pooledBrowser = null;
    }
    return;
  }

  // Dedicated browser cleanup
  await browser.close().catch(() => {});
}

```

**Draining the pool on process exit:**

```typescript
// packages/engine/src/services/browserManager.ts
export async function drainBrowserPool(): Promise<void> {
  const pending = _pooledBrowserLaunchPromise;
  _pooledBrowserLaunchPromise = null;
  if (pending) {
    await pending.then(r => r.browser.close()).catch(() => {});
  }

  if (pooledBrowser) {
    const b = pooledBrowser;
    pooledBrowser = null;
    await b.close().catch(() => {});
  }
}

```

## Related Components

The same pooling logic appears in [`packages/producer/src/services/browserManager.ts`](https://github.com/heygen-com/hyperframes/blob/main/packages/producer/src/services/browserManager.ts) for the producer CLI, ensuring consistent resource management across the monorepo. Configuration defaults reside in [`packages/engine/src/config.ts`](https://github.com/heygen-com/hyperframes/blob/main/packages/engine/src/config.ts), while Chrome argument generation—including mode-specific flag stripping for pooled instances—lives in [`packages/engine/src/services/buildChromeArgs.ts`](https://github.com/heygen-com/hyperframes/blob/main/packages/engine/src/services/buildChromeArgs.ts).

## Summary

- **Single instance sharing**: The `pooledBrowser` variable maintains one Chrome instance per process, shared via `acquireBrowser()` and `releaseBrowser()`.
- **Capture mode safety**: Mode validation prevents incompatible task assignments, spawning dedicated browsers only when modes conflict.
- **Concurrency protection**: `_pooledBrowserLaunchPromise` ensures only one Chrome launches even under high contention.
- **Automatic cleanup**: Reference counting destroys the browser when the last worker releases it; `drainBrowserPool()` handles forced cleanup.
- **Default enabled**: Pooling activates automatically via `DEFAULT_CONFIG.enableBrowserPool` unless explicitly disabled.

## Frequently Asked Questions

### What happens if two workers need different capture modes?

If a worker requests a capture mode (e.g., screenshot) that differs from the pooled instance's mode (e.g., Begin-frame), the pool is not evicted. Instead, that worker receives a **dedicated** browser instance launched specifically for its mode. This ensures that no task receives a browser incapable of its required rendering method, preventing deadlocks.

### How does the pool prevent launching multiple Chrome instances simultaneously?

The `_pooledBrowserLaunchPromise` variable caches the launch promise when the first caller initiates Chrome startup. Subsequent concurrent callers detect this promise and `await` it rather than triggering new launches. Once the promise resolves, the pool populates and the promise clears, eliminating redundant browser startups under high load.

### When does the pooled Chrome instance actually close?

The pooled browser closes only when `releaseBrowser()` decrements the reference count to zero, or when `drainBrowserPool()` forces cleanup. Each call to `acquireBrowser()` increments `pooledBrowserRefCount`, and each `releaseBrowser()` decrements it. When the count hits zero, the system closes the browser and nullifies the pool variables.

### Can I disable the browser pool for specific renders?

Yes. Pass `enableBrowserPool: false` in the EngineConfig when calling `acquireBrowser()`. When disabled, each call launches a fresh Chrome instance that closes immediately upon `releaseBrowser()`, bypassing the shared state entirely. This is useful for debugging or when complete process isolation is required.