# Implications of Undisposed Stubs Accumulating on Peers in Cloudflare Computer RPC

> Learn how undisposed stubs in Cloudflare Computer RPC lead to memory growth, stale streams, and potential DoS. Discover how to prevent issues with proper stub disposal.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-08-13

---

**Undisposed Capnweb stubs in Cloudflare Computer RPC cause unbounded memory growth, stale streams, watermark desynchronization, and potential denial-of-service conditions by preventing remote resource cleanup until explicitly disposed via `[Symbol.dispose]`.**

Undisposed stubs accumulating on peers represent a critical resource management concern in the Cloudflare Computer RPC system. When Capnweb stubs leak across Workspace RPC sessions, they retain references to remote iterators and buffers, causing operational degradation on long-running peers. Understanding these implications requires examining the disposal mechanics implemented in the `cloudflare/computer` repository.

## Operational Impacts of Leaked Stubs on RPC Peers

### Unbounded Memory and Resource Growth

Every stub holds a **result envelope**—often a `ReadableStream`—that maintains references to the remote iterator, buffers, and bookkeeping objects. If the envelope is never disposed, the remote side cannot release those resources, leading to unbounded memory consumption on persistent peers. In [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts), the `maybeDispose` helper explicitly calls `[Symbol.dispose]` on the envelope (lines 39-45) to bound stream lifetimes.

### Stale Streams and Blocked Back-Pressure

An open stream prevents the remote side from cleaning up the underlying generator, effectively stalling subsequent `fetchChanges` calls because the generator remains "busy." The driver handles this by canceling the stream before disposal when watermark divergence is detected, as shown in the cancellation logic at [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) lines 133-146.

### Watermark Desynchronization and Protocol Drift

The RPC contract relies on the remote signaling full batch consumption via envelope disposal. When stubs leak, the remote never receives the "batch-finished" signal, causing watermarks to drift out of sync with the local side. This triggers repeated retries or session aborts. The `assertAppliedPushCursor` checks at lines 70-78 in [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) assume proper envelope teardown; leaking stubs violate this invariant.

### Noisy Leak Detection and False Positives

The repository includes a debug-only leak tracker (`stubSnapshot`) that counts live stub objects per class. Undisposed stubs inflate these counters, generating false-positive leak reports during soak-tests and obscuring genuine memory leaks. This utility resides in [`packages/rpc/src/debug.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/debug.ts) and is validated by [`packages/rpc/tests/debug.test.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/tests/debug.test.ts).

### Multi-Tenant Denial-of-Service Risks

In multi-tenant environments, a misbehaving client that never disposes its stubs can consume substantial portions of a Durable Object's RAM, degrading service for other peers. The design emphasizes **idempotent disposal**—exemplified by the `close()` method in [`client.ts`](https://github.com/cloudflare/computer/blob/main/client.ts) guarding against double-dispose—to encourage correct cleanup patterns.

## Mitigation Strategies in the RPC Layer

### Explicit Client-Side Disposal

The `createSyncClient` function returns a stub with a `close()` method that invokes `[Symbol.dispose]` on the root stub before closing the WebSocket. This pattern, found in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) lines 55-64, ensures that client shutdown triggers proper resource release.

### Driver-Level Envelope Cleanup

Every `pullOnce` call wraps `remote.fetchChanges` results in a `try…finally` block that guarantees `maybeDispose` execution. This defensive programming at [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) lines 39-45 and the final `finally` block at lines 70-73 ensures envelopes release even when operations throw.

### Cancellation on Watermark Divergence

When detecting watermark mismatches, the driver proactively cancels the stream via `fetchResult.stream.cancel()` before resetting cursors. This prevents in-process generators from lingering indefinitely, as implemented in [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts) lines 133-146.

### Debug-Only Stub Tracking

The optional `CAPNWEB_TRACK_STUBS` flag enables `enableStubTracking()`, allowing developers to snapshot live stub counts and assert they return to zero after clean shutdown. This diagnostic tool, located in [`packages/rpc/src/debug.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/debug.ts), helps verify disposal correctness during development.

## Implementation Example and Best Practices

The following pattern demonstrates proper stub lifecycle management using the leak tracker:

```typescript
import { createSyncClient } from "@cloudflare/computer-rpc/client";
import { enableStubTracking, stubSnapshot } from "@cloudflare/computer-rpc/debug";

// Enable stub tracking for testing/debugging only
enableStubTracking();

// Initialize client connection
const client = createSyncClient({ url: "ws://127.0.0.1:45678/rpc" });

await client.fetchChanges({ after: { rev: 0, path: null } });
// ... perform synchronization work ...

// Critical: Close client to dispose root stub
await client.close();

// Verify clean shutdown: all stubs disposed
console.assert(Object.keys(stubSnapshot()).length === 0);

```

Omitting the `close()` call leaves the stub envelope alive, keeping the remote stream open and `stubSnapshot()` counters non-zero, indicating a resource leak.

## Summary

- **Undisposed stubs** retain `ReadableStream` envelopes and remote iterator references, causing unbounded memory growth on peers.
- **Stale generators** block subsequent `fetchChanges` calls and disrupt back-pressure mechanisms until streams are canceled.
- **Watermark drift** occurs when disposal signals fail to transmit, breaking protocol invariants like `assertAppliedPushCursor`.
- **Leak tracking** utilities in [`debug.ts`](https://github.com/cloudflare/computer/blob/main/debug.ts) help identify accumulation during testing, but leaked stubs generate false positives in production monitoring.
- **Proper disposal** requires calling `client.close()` or relying on the driver's `maybeDispose` cleanup in [`sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/sync-driver.ts).

## Frequently Asked Questions

### What causes stubs to accumulate in Cloudflare Computer RPC?

Stubs accumulate when client code fails to invoke `[Symbol.dispose]` on Capnweb stub objects, typically by omitting the `client.close()` call or losing references to result envelopes before the driver can clean them up. Each stub maintains open streams and remote references that persist until explicit disposal occurs.

### How does the RPC driver prevent stub leaks during errors?

The `pullOnce` implementation in [`packages/rpc/src/sync-driver.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/sync-driver.ts) wraps remote calls in `try…finally` blocks that invoke `maybeDispose` (lines 39-45), ensuring envelope cleanup occurs even when exceptions interrupt normal control flow. This defensive pattern guarantees resources release despite application errors.

### Can undisposed stubs affect other tenants in Cloudflare Workers?

Yes. Since undisposed stubs consume Durable Object RAM indefinitely, a single misbehaving client in a multi-tenant environment can exhaust memory quotas, degrading performance for other peers sharing the same infrastructure. The idempotent `close()` method in [`client.ts`](https://github.com/cloudflare/computer/blob/main/client.ts) mitigates this by encouraging definitive resource cleanup.

### How do I detect stub leaks during development?

Enable the `CAPNWEB_TRACK_STUBS` environment flag and import `enableStubTracking` and `stubSnapshot` from [`packages/rpc/src/debug.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/debug.ts). After closing your client, assert that `stubSnapshot()` returns an empty object; non-zero counts indicate undisposed stubs requiring investigation.