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.
Integration with Related Components
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'salarm()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
WorkspaceStubrequires no separate disposal; cleaning the underlyingWorkspacecovers all resources. - Reference
bestEffortCleanupfrom 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →