How to Debug Stub Accumulation Using the Debug Surface and `GET /__computerd/stubs`

Enable COMPUTER_DEBUG=1 and query GET /__computerd/stubs to expose live RpcTarget instance counts, then dispose of parent stubs via client.dispose() to prevent memory leaks in long-running Computer sessions.

The cloudflare/computer platform uses Cap'n Proto-based RPC stubs to represent remote capabilities like fs, shell, and git that live inside the Durable Object (computerd). Because the Cap'n Proto RPC layer does not automatically garbage-collect remote stubs, unused stub objects accumulate over time and cause memory pressure or "stub-leak" failures. This article shows how to use the built-in debug surface to diagnose stub accumulation and fix the root cause.

What Is Stub Accumulation?

Stub accumulation occurs when your code creates RpcTarget stub instances without properly disposing of them. In packages/computer/src/stub.ts, the Stub class wraps remote capabilities as accessor properties (lines 555-618). Each stub maintains a reference to sub-stubs like fs or shell, and these references persist until explicitly released.

The disposal contract is documented in docs/11_lifecycle.md: callers must invoke dispose() on parent stubs to cascade cleanup to all child stubs. Failure to do so leaves RpcTarget instances tracked by the runtime, consuming memory indefinitely.

Enabling the Debug Surface

The computerd CLI exposes stub diagnostics when debug mode is active. You can enable this two ways:

  • CLI flag: --debug
  • Environment variable: COMPUTER_DEBUG=1

When enabled, the server maintains an internal registry of all live RpcTarget instances by subclass name. This registry powers the HTTP debug endpoint without impacting production performance when disabled.

Querying Stub Counts with GET /__computerd/stubs

The debug surface exposes a JSON endpoint at GET /__computerd/stubs. The handler is implemented in packages/computerd/src/cli/computerd.ts (line 236).

The endpoint returns a map of stub class names to live instance counts:

{
  "fs": 12,
  "shell": 5,
  "git": 1,
  "runtime": 3
}

Each key represents an RpcTarget subclass, and the value indicates how many instances currently exist. A steadily increasing count for any stub type signals a leak in your disposal logic.

Example: Fetching Stub Counts from a Test

import { exec } from "child_process";
import fetch from "node-fetch";

process.env.COMPUTER_DEBUG = "1";
exec("npm run start:computerd &", (err) => {
  if (err) throw err;
});

(async () => {
  // Wait for server ready (use proper health checks in production)
  await new Promise((r) => setTimeout(r, 2000));

  const res = await fetch("http://127.0.0.1:8787/__computerd/stubs");
  const stubs = await res.json();
  console.log("Live stub counts:", stubs);
  // Expected for clean state: { fs: 0, shell: 0, git: 0, ... }
})();

Interpreting Stub Accumulation Patterns

Different accumulation patterns point to different root causes:

Pattern Likely Cause Fix
fs steadily climbing File system stubs created per request without disposal Scope fs usage and add dispose()
shell sporadic spikes Shell commands executed without cleanup Wrap in try/finally with disposal
Multiple stubs rising together Parent stub never disposed Ensure client.dispose() called
Count never returns to zero Stub held in closure or global Audit references and weak maps

Query GET /__computerd/stubs before and after test scenarios to isolate which code paths leak stubs.

Proper Stub Disposal to Prevent Accumulation

The packages/computer/src/stub.ts file defines disposal semantics where parent stub cleanup cascades to all sub-stubs. Always call dispose() on the root Computer client when work completes:

import { Computer } from "@cloudflare/computer";

async function runTask() {
  const client = new Computer({ /* connection config */ });
  const { fs, shell } = await client.fsShell(); // stub bundle

  try {
    await fs.writeFile("/tmp/foo.txt", "data");
    await shell.exec("echo hello");
    return { success: true };
  } finally {
    // Critical: dispose parent to release all sub-stubs
    await client.dispose();
  }
}

runTask().catch(console.error);

Without the finally block, exceptions would skip disposal and leak stubs. Use this pattern universally for stub-bearing operations.

Validating Fixes with Stub-Soak Testing

packages/computer/tests/stub-soak.test.ts provides a reference implementation for stress-testing stub lifecycles. The test repeatedly creates and disposes stub bundles, verifying that GET /__computerd/stubs returns zero counts after each iteration.

Run this test while monitoring the debug endpoint to confirm your disposal logic works under load. The pattern demonstrates:

  1. Create stub instances
  2. Perform operations
  3. Call dispose()
  4. Assert all stub counts return to zero via GET /__computerd/stubs

Automating Stub Leak Detection in CI

Add a health-check step to your integration pipeline that fails builds when stub counts exceed thresholds:

// ci/stub-check.ts
async function assertNoStubLeaks(threshold = 5) {
  const res = await fetch("http://localhost:8787/__computerd/stubs");
  const stubs = await res.json();

  const offenders = Object.entries(stubs).filter(([, count]) => count > threshold);
  if (offenders.length > 0) {
    throw new Error(`Stub leak detected: ${JSON.stringify(offenders)}`);
  }
}

Execute this after integration tests to catch regressions before deployment.

Summary

  • Enable debugging with COMPUTER_DEBUG=1 to activate stub tracking in computerd.
  • Query GET /__computerd/stubs to retrieve live RpcTarget counts per stub type.
  • Interpret growth patterns to identify which code paths leak specific stub classes.
  • Dispose parent stubs via client.dispose() to cascade cleanup to all sub-stubs.
  • Validate with soak tests from packages/computer/tests/stub-soak.test.ts.
  • Automate in CI to prevent stub leak regressions.

Frequently Asked Questions

What causes stub accumulation in cloudflare/computer?

Stub accumulation occurs when Cap'n Proto RPC stubs are created without explicit disposal. The RPC layer does not garbage-collect remote capabilities automatically, so each fs, shell, or git stub persists in memory until dispose() is called on its parent, as implemented in packages/computer/src/stub.ts.

How do I interpret the GET /__computerd/stubs response?

The JSON response maps stub class names to live instance counts. In packages/computerd/src/cli/computerd.ts (line 236), the endpoint aggregates RpcTarget instances by their constructor name. Non-zero values are normal during active work, but counts that grow endlessly or never return to baseline indicate disposal failures.

Why must I dispose stubs explicitly instead of relying on garbage collection?

Cap'n Proto maintains strong references to exported capabilities for RPC correctness. The docs/11_lifecycle.md disposal contract explains that JavaScript garbage collection cannot reach across this boundary—only explicit dispose() signals the runtime to release the underlying capability, preventing memory exhaustion in long-running Durable Objects.

Can I use the debug surface in production?

The debug surface adds minimal overhead when enabled, but COMPUTER_DEBUG=1 is designed for development and testing. Production deployments should disable debug mode and rely on CI automation with GET /__computerd/stubs to validate stub hygiene before release, as shown in packages/computer/tests/stub-soak.test.ts.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →