---
description: Replace the Sandbox class from @cloudflare/sandbox 0.12, or your subclass of it, with a Durable Object class that starts its own container.
title: Replace the Sandbox class
image: https://developers.cloudflare.com/sandbox/sdk/migrate/replace-the-sandbox-class/og.png?v=5defa52ddfaf0224
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt  
> Use this file to discover all available pages before exploring further.

# Replace the Sandbox class

Last updated Oct 23, 2026|Copy as Markdown| [View as Markdown](https://developers.cloudflare.com/sandbox/sdk/migrate/replace-the-sandbox-class/index.md)| [Agent setup](https://developers.cloudflare.com/agent-setup/)

Replace the 0.12 `Sandbox` class with a Durable Object class that you write. In 0.12, your Worker calls package methods through `getSandbox()`. In 1.0, your class starts the container through `this.ctx.container`, and your Worker calls the methods that your class defines.

## Before you start

Your application runs `@cloudflare/sandbox` 0.12.14. This page replaces the `Sandbox` class, whether you export it from the package or extend it. It also replaces `getSandbox()` and its options, class fields such as `sleepAfter` and `envVars`, and the hooks `onStart()`, `onStop()`, `onError()`, and `onActivityExpired()`.

The page ends with the Worker running under `wrangler dev`. You cannot undo the deploy that switches your sandboxes to 1.0. Before you deploy, follow [Plan the move to Sandbox SDK 1.0](https://developers.cloudflare.com/sandbox/sdk/migrate/plan-the-move/).

The steps keep your class name, which moves every sandbox in place. To move sandboxes side by side, write the class under a new name, such as `SandboxV1`, and add its Wrangler configuration as [Move sandboxes side by side](https://developers.cloudflare.com/sandbox/sdk/migrate/move-side-by-side/) describes.

## Replace the class

1. Install version 1 of the package:npmyarnpnpmbun

   ```
   npm i @cloudflare/sandbox
   ```

   ```
   yarn add @cloudflare/sandbox
   ```

   ```
   pnpm add @cloudflare/sandbox
   ```

   ```
   bun add @cloudflare/sandbox
   ```

2. Replace the `FROM docker.io/cloudflare/sandbox` line in your `Dockerfile` with a base image that has the tools your commands use, and copy the `sandbox-shim` helper into it:

   *Dockerfiledockerfile*

   

   ```dockerfile
   FROM node:24-trixie-slim

   RUN apt-get update \
   	&& apt-get install -y --no-install-recommends \
   		ca-certificates git python3 \
   	&& rm -rf /var/lib/apt/lists/*

   COPY --from=docker.io/cloudflare/sandbox:1.0.1 /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim

   WORKDIR /workspace
   CMD ["sleep", "infinity"]
   ```

   The 0.12 image included Python, Node.js, and Git. Keep the ones your commands use. `Files`, `S3Mount`, and `DirectoryBackup` need `sandbox-shim`. Match its tag to the `@cloudflare/sandbox` version that you installed. Remove `EXPOSE` lines, which 1.0 does not use. Commands that `exec()` runs do not see `ENV` lines, so pass those values in the `env` option instead.
3. Change the `containers` entry for your class in the Wrangler configuration. Keep your class name and its binding, so each name reaches the Durable Object and storage it reached in 0.12. Replace `my-worker-mysandbox-v2` with a name that your account does not use yet:

   ```jsonc
   {
   	"containers": [
   		{
   			"class_name": "MySandbox",
   			"name": "my-worker-mysandbox-v2",
   			"scheduling_policy": "durable_object",
   			"images": {
   				"sandbox": {
   					"dockerfile": "./Dockerfile",
   				},
   			},
   		},
   	],
   }
   ```

   ```toml
   [[containers]]
   class_name = "MySandbox"
   name = "my-worker-mysandbox-v2"
   scheduling_policy = "durable_object"

   [containers.images.sandbox]
   dockerfile = "./Dockerfile"
   ```

   The new entry makes these changes:
   - `scheduling_policy: "durable_object"` lets each Durable Object start its own container. This policy does not accept `image`, `instance_type`, or `max_instances`, so remove them. The `Dockerfile` moves to `images`, and the instance type moves to `start()`.
   - The container application gets a new `name`. Without one, the new application takes the name that Wrangler generated for the old application, and the deploy fails after the new Worker version is already live.
   - Leave `durable_objects` and your `migrations` or `exports` as they are. The class keeps its name, so it needs no class migration.

   Also remove `SANDBOX_TRANSPORT`, `SANDBOX_INSTANCE_TIMEOUT_MS`, `SANDBOX_PORT_TIMEOUT_MS`, and `SANDBOX_POLL_INTERVAL_MS` from `vars`, and keep `nodejs_compat`, which 1.0 requires.
4. Check your compatibility date.

   Note

   `this.ctx.exports` is `undefined` before compatibility date `2025-11-17`, unless you add the `enable_ctx_exports` compatibility flag. The migration pages for outbound rules and bucket mounts use it. Several 0.12 templates set `2025-05-06`.
5. Generate types for the new configuration:npmyarnpnpm

   ```
   npx wrangler types
   ```

   ```
   yarn wrangler types
   ```

   ```
   pnpm wrangler types
   ```

6. Write the class. In this example, the 0.12 subclass sets a lifetime and environment variables, and overrides `onStart()`:

   *src/index.ts (0.12)ts*

   

   ```ts
   import { getSandbox, Sandbox } from "@cloudflare/sandbox";

   export class MySandbox extends Sandbox<Env> {
   	sleepAfter = "30m";
   	envVars = { NODE_ENV: "test" };

   	override async onStart() {
   		await this.ctx.storage.put("startedAt", Date.now());
   	}
   }
   ```

   In 1.0, the class extends `DurableObject` and starts the container itself:

   *src/index.ts (1.0)ts*

   

   ```ts
   import { DurableObject } from "cloudflare:workers";

   // Replaces sleepAfter and envVars.
   const INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000;
   const ENV = { NODE_ENV: "test" };

   export class MySandbox extends DurableObject<Env> {
   	constructor(ctx: DurableObjectState, env: Env) {
   		super(ctx, env);
   		const container = ctx.container;
   		// A restarted Durable Object sets the timeout again.
   		if (container?.running) {
   			void ctx.blockConcurrencyWhile(() =>
   				container.setInactivityTimeout(
   					INACTIVITY_TIMEOUT_MS,
   				),
   			);
   		}
   	}

   	private get container(): Container {
   		const container = this.ctx.container;

   		if (!container) {
   			throw new Error("No container is configured");
   		}

   		return container;
   	}

   	private setup: Promise<void> | null = null;

   	private ensureRunning(): Promise<void> {
   		// Set up each new container, and a running container
   		// after this Durable Object restarts.
   		if (this.setup === null || !this.container.running) {
   			this.setup = this.startContainer().catch((error) => {
   				this.setup = null;
   				throw error;
   			});
   		}
   		return this.setup;
   	}

   	private async startContainer(): Promise<void> {
   		const newContainer = !this.container.running;

   		if (newContainer) {
   			this.container.start({
   				image: this.container.images.sandbox,
   				instance: "standard-1",
   				env: ENV,
   				// 0.12 allowed Internet access by default.
   				enableInternet: true,
   			});
   		}

   		try {
   			if (newContainer) {
   				// Replaces onStart().
   				await this.ctx.storage.put("startedAt", Date.now());
   			}
   			await this.container.setInactivityTimeout(
   				INACTIVITY_TIMEOUT_MS,
   			);
   		} catch (error) {
   			// The next request starts a new container.
   			await this.container.destroy();
   			throw error;
   		}
   	}
   }
   ```

   Call `ensureRunning()` before each use of the container. It runs `startContainer()` once for each new container, and once more when a restarted Durable Object finds the container running. A deploy restarts the Durable Object and can stop setup partway, so every step after `start()` must be safe to run again. `running` is `true` as soon as `start()` returns, so requests that arrive during setup wait for the same `startContainer()` call. If a setup step throws, `startContainer()` stops the container, so no request runs commands in a container that is not set up.

   `start()` returns before the container is ready, and the first `exec()` waits for it. The inactivity timeout does not survive a Durable Object restart. The constructor sets it again when a restarted Durable Object finds the container running. The timeout accepts at most 6 hours. If your commands do not need the Internet, set `enableInternet` to `false`.
7. Replace each class field and `getSandbox()` option that your code sets:

   | 0.12 | 1.0 |
   | --- | --- |
   | `sleepAfter` | `setInactivityTimeout()` after `start()`, and again in the constructor. |
   | `keepAlive` | An [alarm](https://developers.cloudflare.com/durable-objects/api/alarms/) that runs more often than the inactivity timeout while work remains. |
   | `envVars`, `setEnvVars()` | `env` on `start()` for the main process, and `env` on each `exec()` call. Commands do not inherit the `start()` variables, except `PATH`. |
   | `entrypoint` | `entrypoint` on `start()`, or `CMD` in the image. |
   | `enableInternet` | `enableInternet` on `start()`, which every call must pass. 0.12 defaulted to `true`. |
   | `labels`, `setLabels()` | `labels` on `start()`. |
   | `defaultPort`, `requiredPorts` | `this.container.getTcpPort(port)`. Refer to [Move preview URLs](https://developers.cloudflare.com/sandbox/sdk/migrate/preview-urls/). |
   | `allowedHosts`, `deniedHosts`, `outboundByHost`, `outbound`, `interceptHttps` | Refer to [Move outbound rules](https://developers.cloudflare.com/sandbox/sdk/migrate/outbound-traffic/). |
   | `normalizeId` | Lowercase the name before `getByName()`. |
   | `containerTimeouts` | An `AbortSignal` on the calls that need a deadline. Refer to [Replace timeouts](https://developers.cloudflare.com/sandbox/sdk/migrate/commands/#replace-timeouts). |
   | `enableDefaultSession` | Nothing. Each `exec()` call is its own process. Refer to [Replace sessions](https://developers.cloudflare.com/sandbox/sdk/migrate/commands/#replace-sessions). |
   | `transport`, `pingEndpoint` | Nothing. Remove them. |
8. Replace the hooks your subclass overrides:
   - `onStart()`: run the code in `startContainer()`, inside `if (newContainer)`, as in the class from the previous step.
   - `onStop()` and `onError()`: watch the container with [`monitor()`](https://developers.cloudflare.com/containers/api/durable-object-container/#monitor), and catch errors where you call `start()` and `exec()`.
   - `onActivityExpired()`: no code runs before the inactivity timeout stops the container. To run code first, keep your own idle deadline in an alarm, and call `this.container.destroy()` when it passes. For an example that saves the sandbox before it stops, refer to [Save a sandbox automatically](https://developers.cloudflare.com/sandbox/files/save-a-sandbox-automatically/).

   To watch the container, call a method like this one in the `try` block of `startContainer()`. A restarted Durable Object runs `startContainer()` again, so it watches the running container too:

   *src/index.tsts*

   

   ```ts
   export class MySandbox extends DurableObject<Env> {
   	// ...

   	private watch(): void {
   		this.container.monitor().then(
   			() => console.log("Exited with code 0"),
   			(error: unknown) => console.error("Stopped", error),
   		);
   	}
   }
   ```

   `monitor()` resolves when the main process exits with code `0` or `destroy()` stops the container without a reason. It rejects when the process fails or `destroy()` passes a reason. The call ends when the Durable Object leaves memory, so it misses a stop that happens before the next request. While the call keeps the Durable Object in memory, the inactivity timeout does not start, and the container keeps running past the timeout. For more information, refer to [Sandbox lifetime](https://developers.cloudflare.com/sandbox/concepts/lifetime/#what-stops-an-instance). For a record of every stop, refer to [Run code when a sandbox stops](https://developers.cloudflare.com/sandbox/manage/run-code-when-a-sandbox-stops/).
9. Move the calls your Worker makes into methods on the class. In 0.12, the Worker calls package methods on the stub that `getSandbox()` returns:

   *src/index.ts (0.12)ts*

   

   ```ts
   export default {
   	async fetch(request: Request, env: Env): Promise<Response> {
   		const id = new URL(request.url).pathname.slice(1);
   		const sandbox = getSandbox(env.MY_SANDBOX, id, {
   			normalizeId: true,
   		});
   		const result = await sandbox.exec("npm test", {
   			cwd: "/workspace/app",
   		});

   		return Response.json({
   			exitCode: result.exitCode,
   			output: result.stdout,
   		});
   	},
   };
   ```

   In 1.0, the stub has only the methods your class defines. Add a method to `MySandbox`, and call it from the Worker:

   *src/index.ts (1.0)ts*

   

   ```ts
   export class MySandbox extends DurableObject<Env> {
   	// ...

   	async test(): Promise<{ exitCode: number; output: string }> {
   		await this.ensureRunning();

   		const process = await this.container.exec(["npm", "test"], {
   			cwd: "/workspace/app",
   			env: ENV,
   		});
   		const result = await process.output();

   		return {
   			exitCode: result.exitCode,
   			output: new TextDecoder().decode(result.stdout),
   		};
   	}
   }
   ```

   *src/index.ts (1.0)ts*

   

   ```ts
   export default {
   	async fetch(request: Request, env: Env): Promise<Response> {
   		const id = new URL(request.url).pathname.slice(1);
   		// Replaces normalizeId: true.
   		const sandbox = env.MY_SANDBOX.getByName(id.toLowerCase());

   		return Response.json(await sandbox.test());
   	},
   } satisfies ExportedHandler<Env>;
   ```

   Move the methods that your 0.12 subclass defines into the 1.0 class. Your Worker calls them on the stub as it did in 0.12. Inside those methods, replace `this.exec()` and other package calls with calls on `this.container`. For each kind of call, refer to [Change command calls](https://developers.cloudflare.com/sandbox/sdk/migrate/commands/) and the feature pages in [Migrate from Sandbox SDK 0.x](https://developers.cloudflare.com/sandbox/sdk/migrate/).

## Check the class

Start your Worker locally:

npmyarnpnpm

```
npx wrangler dev
```

```
yarn wrangler dev
```

```
pnpm wrangler dev
```

Wrangler builds the image when it starts. Send a request to each route that uses a sandbox. The first request for each name starts a container and waits for it. Each route returns what it returned on 0.12.

## Next steps

- To deploy the new class, follow [Plan the move to Sandbox SDK 1.0](https://developers.cloudflare.com/sandbox/sdk/migrate/plan-the-move/).
- To change `exec()` and sessions, refer to [Change command calls](https://developers.cloudflare.com/sandbox/sdk/migrate/commands/).
- To change file calls, refer to [Change file calls](https://developers.cloudflare.com/sandbox/sdk/migrate/files/).
- For the methods on `this.ctx.container`, refer to the [Container API](https://developers.cloudflare.com/containers/api/durable-object-container/).

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/sdk/migrate/replace-the-sandbox-class/#page","headline":"Replace the Sandbox class","description":"Replace the Sandbox class from @cloudflare/sandbox 0.12, or your subclass of it, with a Durable Object class that starts its own container.","url":"https://developers.cloudflare.com/sandbox/sdk/migrate/replace-the-sandbox-class/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/sdk/migrate/replace-the-sandbox-class/og.png?v=5defa52ddfaf0224","dateModified":"2026-10-23","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
