Implications of Undisposed Stubs Accumulating on Peers in Cloudflare Computer RPC
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, 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 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 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 and is validated by 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 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 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 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 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, helps verify disposal correctness during development.
Implementation Example and Best Practices
The following pattern demonstrates proper stub lifecycle management using the leak tracker:
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
ReadableStreamenvelopes and remote iterator references, causing unbounded memory growth on peers. - Stale generators block subsequent
fetchChangescalls 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.tshelp identify accumulation during testing, but leaked stubs generate false positives in production monitoring. - Proper disposal requires calling
client.close()or relying on the driver'smaybeDisposecleanup insync-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 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 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. After closing your client, assert that stubSnapshot() returns an empty object; non-zero counts indicate undisposed stubs requiring investigation.
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 →