How to Implement Workspace Disposal in a Durable Object for Resource Cleanup

Call workspace.close() in your Durable Object's alarm() handler or explicit teardown method to release all backend connections, clear caches, and reset internal state.

Durable Objects on Cloudflare Workers provide stateful compute with persistent storage, but they require explicit cleanup to prevent resource leaks. When building applications with the cloudflare/computer repository, the Workspace class manages local SQLite storage, execution backends, and RPC connections to remote processes. Proper disposal ensures graceful shutdowns and prevents dangling handles when a Durable Object terminates or encounters fatal errors.

What Workspace.close() Does Under the Hood

The close() method in packages/computer/src/workspace.ts performs a four-step teardown sequence designed to make the instance safe for garbage collection:

1. Increment Connection Generation

The method first bumps this.#connectionGeneration to prevent any in-flight RPC connection from being reused after shutdown:

this.#connectionGeneration += 1;

Source: workspace.ts#L665

This acts as a circuit breaker—any pending connection attempts will see the generation mismatch and abort rather than establishing stale links.

2. Clear All Caches

All handle maps are purged so subsequent calls trigger reconnection rather than returning dead references:

this.#handles.clear();
this.#moduleHandles.clear();
this.#shells.clear();
this.#commandHandles.clear();
this.#connecting.clear();

Source: workspace.ts#L666-L680

3. Close Backend Handles

Each cached handle may own a network socket or remote process. close() awaits their individual close() methods in parallel:

await Promise.all([...handles, ...moduleHandles].map(async (h) => {
  await h.close?.();
}));

Source: workspace.ts#L688-L690

Handles without a close method are skipped gracefully via optional chaining.

4. Reset Ready Promise

The ready promise is cleared so future ready() calls re-initialize mounts and backends:

this.#readyPromise = undefined;

Source: workspace.ts#L681

After these steps complete, the Workspace holds no cached RPC connections, no pending mutation queues, and all local resources are eligible for garbage collection.

When to Dispose a Workspace in Your Durable Object

Three scenarios require explicit disposal:

  • Durable Object termination – Cloudflare invokes alarm() during graceful shutdown; this is your only hook for cleanup.
  • Explicit session teardown – User-initiated "stop" commands or admin operations that must free resources immediately.
  • Error-path cleanup – Fatal errors that render the DO unusable; call close() before returning error responses to prevent leaks.

Complete Implementation Example

Below is a production-ready Durable Object that properly initializes and disposes its Workspace:

import { DurableObject } from "cloudflare:workers";
import { Workspace } from "@cloudflare/computer";

export interface Env {
  // Your environment bindings
}

export class ComputeDO extends DurableObject {
  #workspace: Workspace;

  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
    
    // Initialize with DO storage as persistence layer
    this.#workspace = new Workspace({
      storage: state.storage,
      backends: [{ 
        id: "default", 
        type: "shell", 
        connect: () => this.#createBackendConnection(env) 
      }],
    });
  }

  async fetch(request: Request): Promise<Response> {
    // Ensure workspace is ready before use
    await this.#workspace.ready();
    
    const path = new URL(request.url).pathname;
    const content = await this.#workspace.fs.readFile(path);
    
    return new Response(content ?? "Not found", {
      status: content ? 200 : 404
    });
  }

  // Called by Cloudflare during DO shutdown or alarm trigger
  async alarm(): Promise<void> {
    await this.#workspace.close();
  }

  // Optional: expose explicit disposal for admin operations
  async dispose(): Promise<{ success: boolean }> {
    await this.#workspace.close();
    return { success: true };
  }

  #createBackendConnection(env: Env): any {
    // Implementation depends on your backend configuration
    return env.BACKEND_SERVICE.connect();
  }
}

Handling Disposal Outside of alarm()

For scenarios requiring cleanup outside the shutdown lifecycle, expose a dedicated disposal method:

async fetch(request: Request): Promise<Response> {
  const url = new URL(request.url);
  
  if (url.pathname === "/dispose" && request.method === "POST") {
    await this.#workspace.close();
    return new Response("Workspace disposed", { status: 200 });
  }
  
  // Normal operation...
}

Note that after close(), subsequent operations will trigger re-initialization via ready() if you continue using the same instance.

Understanding how disposal interacts with other cloudflare/computer components prevents subtle bugs:

Component Interaction with close()
WorkspaceStub The stub at packages/computer/src/stub.ts is a stateless RPC façade—it does not own resources, so disposing the underlying Workspace is sufficient.
CommandExecutor Each executor holds cached backend handles. When close() clears #shells and #commandHandles, in-flight commands either finish gracefully or abort via #invalidateHandle transport-error handling.
SyncRetryScheduler Pending retry intents are abandoned when the DO's storage clears; no explicit cleanup needed.

Real-World Pattern from the Repository

The think-compare-runtimes example demonstrates production cleanup with bestEffortCleanup:

// From examples/think-compare-runtimes/worker/think/agents.ts
await bestEffortCleanup(
  "Workspace session close",
  () => workspace.close(),
);

Source: agents.ts#L362-L364

The bestEffortCleanup wrapper logs failures without throwing, ensuring shutdown proceeds even if individual handles fail to close.

Key Source Files

File Purpose
packages/computer/src/workspace.ts Core Workspace class and close() implementation
packages/computer/src/stub.ts Lightweight WorkspaceStub RPC forwarding
examples/think-compare-runtimes/worker/think/agents.ts Production cleanup pattern with bestEffortCleanup
packages/computer/tests/workspace.test.ts Unit tests verifying cache drops and handle closure

Summary

  • Always call await workspace.close() in your Durable Object's alarm() handler to prevent resource leaks during shutdown.
  • The close() method increments connection generation, clears all handle caches, closes backend connections, and resets the ready promise for clean re-initialization.
  • For explicit teardown, expose a dispose() method that callers can invoke via HTTP or other triggers.
  • The WorkspaceStub requires no separate disposal; cleaning the underlying Workspace covers all resources.
  • Reference bestEffortCleanup from the repository examples for resilient production patterns that handle partial failures gracefully.

Frequently Asked Questions

What happens if I don't call close() on my Workspace?

Without close(), cached backend handles remain open, potentially leaking network sockets and remote process connections. The Durable Object may also retain in-memory caches that prevent garbage collection, increasing memory usage across invocations. While Cloudflare eventually evicts the DO, explicit cleanup ensures deterministic resource release.

Can I reuse a Workspace after calling close()?

Yes, but it reinitializes. Subsequent calls to ready() will re-index mounts and reconnect backends because close() resets #readyPromise to undefined. However, for clarity, most applications should treat close() as terminal and instantiate a fresh Workspace if needed.

How does handle closure work if a backend is unresponsive?

The close() implementation uses Promise.all with optional chaining (h.close?.()), so unresponsive backends time out individually without blocking other cleanup. The bestEffortCleanup wrapper in the examples captures and logs these failures while allowing shutdown to proceed.

Should I dispose WorkspaceStub separately?

No. The WorkspaceStub at packages/computer/src/stub.ts is a thin RPC client with no owned resources. Disposing the underlying Workspace instance that the stub targets is sufficient for complete cleanup.

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 →