How Camofox-Browser Implements Lazy Browser Launch and Idle Shutdown
Camofox-browser defers Firefox (Camoufox) instance startup until the first client request and automatically terminates the browser after a configurable period of inactivity, using a singleton promise pattern to serialize concurrent launches and prevent resource waste.
The jo-inc/camofox-browser project provides a resource-efficient HTTP API for browser automation by keeping Firefox instances alive only when active work exists. Its lazy browser launch and idle shutdown architecture ensures that headless browsers start on-demand and shut down automatically when idle, minimizing memory and CPU consumption across deployments.
Core Architecture Components
The implementation splits responsibility across three cooperating layers in server.js: the launch guarantee, the instance factory, and the idle scheduler.
Lazy Launch Guarantee with ensureBrowser()
Located at lines 667–694 in server.js, the ensureBrowser() function acts as the single entry point for all browser-dependent operations. It maintains a module-level browserLaunchPromise that guarantees only one launch attempt executes even when multiple concurrent requests arrive simultaneously.
If the browser instance exists but has disconnected, ensureBrowser() tears down all dead sessions, clears any active virtual display, and forces a fresh launch. This defensive check ensures API resilience against crashed browser processes.
Browser Instance Creation in launchBrowserInstance()
The launchBrowserInstance() function performs the actual heavy lifting referenced within server.js. It selects the host OS, optionally initializes an X-vfb virtual display, configures proxy settings if enabled, builds Playwright launch options, and starts the Firefox (Camoufox) process.
When rotating proxies are configured, the function runs a Google probe with retry logic up to maxAttempts to verify connectivity before considering the launch successful. Upon completion, it stores the browser reference in the module-level browser variable and registers the display for cleanup tracking.
Idle Shutdown Scheduling via scheduleBrowserIdleShutdown()
After every request completion, the server invokes scheduleBrowserIdleShutdown(). This scheduler checks whether any active sessions remain (sessions.size === 0). If the session map is empty, it sets a timeout using BROWSER_IDLE_TIMEOUT_MS (defaulting to 300,000 ms or 5 minutes).
When the timer fires, the system logs "browser idle shutdown (no sessions)", closes the browser instance, and clears the global reference. The companion clearBrowserIdleTimer() function resets the scheduler whenever new activity occurs, preventing premature shutdown during active use.
Request Lifecycle Flow
Understanding the sequence clarifies how resource efficiency is achieved:
- First Request Arrives:
ensureBrowser()detectsbrowserisnulland creates a singletonbrowserLaunchPromise, then callslaunchBrowserInstance(). - Browser Initialization: The instance launches with optional X-vfb, proxy verification, and Playwright configuration, storing the result in the global
browservariable. - Concurrent Request Handling: Additional requests arriving during launch await the same
browserLaunchPromise, preventing duplicate browser processes. - Session Completion: When clients close tabs or requests finish, the session map updates.
scheduleBrowserIdleShutdown()evaluates the empty state. - Idle Detection: If
sessions.sizeremains zero for the configured timeout duration, the browser closes and the reference clears. - Subsequent Requests: The next API call triggers step 1 again, lazily relaunching the browser only when needed.
Implementation Examples
Integrating Lazy Launch in API Routes
The following pattern from server.js demonstrates how routes leverage the architecture:
app.post('/tabs/:tabId/navigate', async (req, res) => {
// … validation omitted …
try {
// Guarantees a live browser; launches lazily if needed.
const b = await ensureBrowser(); // ← lazy launch
const session = await getSession(userId); // creates a new Playwright context
const page = await session.context.newPage();
await page.goto(req.body.url, { timeout: NAVIGATE_TIMEOUT_MS });
// … respond with snapshot …
} finally {
// Ensure idle shutdown timer is refreshed after each request.
scheduleBrowserIdleShutdown(); // ← idle‑shutdown scheduling
}
});
Manual Shutdown Control
For administrative or testing scenarios, you can force immediate cleanup bypassing the idle timer:
// Force immediate shutdown, ignoring the idle timer.
if (browser) {
log('info', 'manual shutdown');
const b = browser;
browser = null;
await b.close();
}
Subprocess Isolation with launchServer()
The architecture separates environment handling from the main server through launchServer() in lib/launcher.js. This wrapper spawns the HTTP API as a subprocess, ensuring server.js never reads process.env directly or spawns child processes other than the isolated browser instance.
import { launchServer } from './lib/launcher.js';
const proc = launchServer({
pluginDir: __dirname,
port: 9377,
env: { CAMOFOX_API_KEY: 'secret' },
log: console,
});
proc.on('exit', code => console.log('server exited', code));
Configuration and Key Files
| File | Responsibility | Key Exports/Functions |
|---|---|---|
server.js |
Main HTTP server, lazy-launch logic, idle-shutdown scheduler | ensureBrowser(), launchBrowserInstance(), scheduleBrowserIdleShutdown() |
lib/launcher.js |
Subprocess isolation and environment management | launchServer() |
lib/config.js |
Centralized environment configuration | browserIdleTimeoutMs, proxy settings, launch options |
lib/metrics.js |
Optional Prometheus instrumentation | Lazily loaded metrics to avoid interfering with idle logic |
The lib/config.js module centralizes all process.env access, providing browserIdleTimeoutMs and other launch parameters without polluting the main server logic. For observability, lib/metrics.js offers Prometheus instrumentation that loads lazily to avoid interfering with shutdown timing.
Summary
- On-demand startup: The
ensureBrowser()singleton promise pattern ensures Firefox launches only when the first request arrives, with concurrent callers waiting on the same launch operation. - Automatic cleanup:
scheduleBrowserIdleShutdown()monitors session counts and closes the browser afterBROWSER_IDLE_TIMEOUT_MS(default 5 minutes) of inactivity. - Resource safety: Disconnected browser detection and virtual display cleanup in
ensureBrowser()prevent resource leaks from crashed instances. - Process isolation:
launchServer()inlib/launcher.jsseparates the HTTP API from environment handling, supporting supervisor-based deployments.
Frequently Asked Questions
How does camofox-browser prevent race conditions during concurrent launches?
When multiple requests arrive simultaneously while no browser instance exists, ensureBrowser() creates a single browserLaunchPromise that all callers await. This singleton promise pattern serializes access to launchBrowserInstance(), ensuring only one Firefox process starts despite concurrent API requests, preventing resource waste and port conflicts.
What configuration controls the idle shutdown timeout?
The BROWSER_IDLE_TIMEOUT_MS environment variable, centralized in lib/config.js, sets the milliseconds of inactivity before automatic shutdown (defaulting to 300,000 ms or 5 minutes). Adjusting this value in the environment changes how long the browser remains running after the last session closes.
How does the architecture handle unexpected browser crashes?
The ensureBrowser() function detects disconnected browser states before attempting operations. Upon detecting a dead instance, it clears the global reference, tears down associated sessions, cleans up virtual displays (X-vfb), and triggers a fresh launch on the next request, ensuring API resilience without manual intervention.
Why use a subprocess launcher instead of running directly in the main process?
The launchServer() function in lib/launcher.js isolates the HTTP server as a subprocess to prevent the main server.js from reading environment variables directly or spawning unintended child processes. This separation supports external supervisors (like systemd or containers) that manage the server lifecycle independently while keeping the browser automation logic clean and environment-agnostic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →