# How the gstack Browser Daemon Achieves Sub‑Second Latency

> Discover how the gstack browser daemon achieves sub-second latency using a persistent local Chromium process and efficient HTTP POST requests, replacing slow protocols.

- Repository: [Garry Tan/gstack](https://github.com/garrytan/gstack)
- Tags: internals
- Published: 2026-05-15

---

**The gstack browser daemon achieves sub‑second latency by maintaining a persistent local Chromium process via Playwright, eliminating the ~3‑second startup cost after the first call, and exposing a thin HTTP interface on `127.0.0.1` that replaces heavyweight WebSocket or JSON‑RPC protocols with simple POST requests.**

The `garrytan/gstack` repository implements a browser automation system that prioritizes execution speed through architectural decisions designed specifically to minimize per‑command overhead. Unlike traditional automation solutions that spawn a new browser instance for every operation, the gstack browser daemon sub‑second latency strategy relies on a long‑running background process and a minimalist CLI client that communicates over local HTTP.

## Persistent Chromium Daemon Architecture

At the core of the performance profile is a **persistent local Chromium daemon** managed by Playwright. According to [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 76‑84, the daemon starts once—taking approximately 3 seconds on the first call—and remains resident in memory for subsequent commands. The browser lifecycle logic resides in [`browse/src/browser-manager.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/browser-manager.ts), which maintains active page contexts and tab references without relaunching the Chromium binary.

### Eliminating Cold Start Penalties

Traditional browser automation tools typically incur a multi‑second penalty on every command because they launch a fresh browser process each time. In gstack, after the initial warm‑up, subsequent `$B` invocations only send a tiny HTTP POST to `127.0.0.1`, costing roughly **100–200 ms** per command as documented in [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 70‑78. This architectural choice shifts the latency bottleneck from process creation to the actual browser operation itself, which typically executes in a few milliseconds.

## Thin HTTP Client Design

The CLI client—implemented in [`browse/src/cli.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cli.ts)—operates as a **thin stateless wrapper** rather than a heavy protocol handler. It reads a tiny local state file, constructs a plain HTTP request, and prints raw stdout output. As noted in [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 75‑82, this design avoids embedding JSON‑RPC or WebSocket layers, eliminating schema‑marshalling and connection‑setup latency. The round‑trip time is effectively reduced to the local network hop (≈ 1 ms) plus the time required for Playwright to execute the native `page.goto` or `page.locator` call.

## Zero‑Overhead Authentication

Authentication overhead is minimized through **local bearer token storage**. The token registry in [`browse/src/token-registry.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/token-registry.ts) manages root, setup‑key, and scoped tokens, but for local calls, the bearer token is cached in the CLI state file and sent automatically with each request. Because the token is read from disk rather than negotiated over the network, no extra I/O or round‑trip latency is added to the critical path, keeping per‑call costs constant and tiny.

## Command Batching for Throughput

For workflows requiring multiple sequential operations, the daemon exposes a **`/batch` endpoint** that aggregates commands into a single HTTP request. As described in [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 28‑33, this batching capability allows remote agents to group many small commands—such as text extraction, element clicks, and snapshots—into one round‑trip, avoiding the ~100–200 ms per‑request penalty. Even when using single commands, the latency remains well under one second.

## Automatic Idle Lifecycle Management

Resource management is handled by an **idle shutdown timer** defined in [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 166‑176. The daemon automatically stops after 30 minutes of inactivity, freeing system resources without requiring manual intervention. This ensures that the 3‑second startup penalty only occurs after a significant pause, maintaining sub‑second responsiveness during active development sessions while preventing resource leaks.

## Performance Profile and Usage Examples

The documented performance characteristics break down as follows:

- **First call**: ~3 seconds (daemon initialization and Chromium launch)
- **Subsequent calls**: ~100–200 ms per command
- **Local loopback latency**: ~1 ms (negligible compared to browser execution)

### Basic CLI Usage

```bash

# First use – daemon starts (≈3 s)

$B status               # prints daemon health

# Subsequent calls – sub‑second

$B text                # fetch clean page text (≈150 ms)

$B click @e30          # click an element from a snapshot (≈180 ms)

$B screenshot page.png # take a screenshot (≈200 ms)

```

### Batch Command Execution

```bash

# Multiple commands in one HTTP round‑trip

$B batch <<EOF
{"commands":[
  {"command":"text"},
  {"command":"click","args":["@e5"]},
  {"command":"snapshot","args":["-i"]}
]}
EOF

```

### Programmatic TypeScript Client

```typescript
import { BrowseClient } from "./browse/src/browse-client.ts";

const client = new BrowseClient({ port: 12345, token: "root-token" });
await client.runCommand("goto", ["https://news.ycombinator.com"]); // ≈120 ms
await client.runCommand("text");                                 // ≈150 ms

```

## Key Implementation Files

| Purpose | File Path |
|---|---|
| **Daemon HTTP server** – request routing and dual‑listener architecture | [`browse/src/server.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/server.ts) |
| **Thin CLI client** – state file parsing and HTTP POST transmission | [`browse/src/cli.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cli.ts) |
| **Chromium lifecycle** – persistent browser instance and tab management | [`browse/src/browser-manager.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/browser-manager.ts) |
| **Token handling** – local authentication and rate‑limiting logic | [`browse/src/token-registry.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/token-registry.ts) |
| **Command definitions** – ~70 supported browser actions | [`browse/src/commands.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/commands.ts) |
| **Batch processing** – multi‑command aggregation logic | [`browse/src/batch.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/batch.ts) |
| **Snapshot engine** – ARIA tree generation and reference resolution | [`browse/src/snapshot.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/snapshot.ts) |
| **Performance documentation** – startup vs. per‑call timing specifications | [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) |

## Summary

- **Persistent daemon**: A long‑running Playwright process in [`browse/src/browser-manager.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/browser-manager.ts) eliminates the ~3‑second Chromium launch cost after the first call.
- **HTTP over localhost**: The CLI in [`browse/src/cli.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cli.ts) uses plain HTTP on `127.0.0.1` instead of WebSockets or JSON‑RPC, reducing protocol overhead to ~1 ms.
- **Local token storage**: Authentication tokens are cached locally, removing network negotiation from the request path.
- **Command batching**: The `/batch` endpoint aggregates multiple operations into a single HTTP request, optimizing throughput for complex workflows.
- **Idle shutdown**: Automatic termination after 30 minutes of inactivity balances resource usage with sub‑second responsiveness.

## Frequently Asked Questions

### How long does the gstack browser daemon take to start?

The initial cold start takes approximately **3 seconds** to launch the Playwright‑managed Chromium instance. Every subsequent command executes in **100–200 milliseconds** because the daemon remains resident in memory, as documented in [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 70‑78.

### What communication protocol does the gstack CLI use?

The CLI communicates with the daemon via **plain HTTP on `127.0.0.1`** rather than WebSockets, JSON‑RPC, or the Chrome DevTools Protocol. This thin client architecture, implemented in [`browse/src/cli.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cli.ts), eliminates connection setup and schema serialization overhead.

### Why does gstack use Playwright instead of Selenium?

Playwright provides **native, low‑level Chromium control** without the additional abstraction layers found in Selenium or CDP wrappers. According to the implementation in [`browse/src/browser-manager.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/browser-manager.ts), raw Playwright API calls such as `page.goto` and `page.locator` execute with minimal overhead, limited only by the browser operation itself.

### How does the daemon maintain security without adding latency?

Security is handled through **local state file authentication**. The bearer token is stored in a local state file and attached automatically to each HTTP request, as described in [`BROWSER.md`](https://github.com/garrytan/gstack/blob/main/BROWSER.md) lines 75‑78. Because no network handshake or token negotiation occurs, authentication adds effectively zero milliseconds to the sub‑second response time.