Skip to content
sandbox-workers
Esc
navigateopen⌘Jpreview
On this page

Get started with stateful mode

Deploy a runtime Worker, host a Sandbox Durable Object in your own Worker, and run your first code context.

1. Deploy a runtime Worker per language

Deploy one private runtime Worker for each language you want to use. This guide uses two: Python and JavaScript.

Open Deploy a runtime Worker and choose Python. In Cloudflare, select your account and a unique Worker name. Keep the detected build and deploy commands. The templates require a Paid Workers plan for their configured CPU allowance.

Record the deployed name, such as sandbox-python. A successful runtime deployment has no public URL; this is intentional.

Open Deploy a runtime Worker again and repeat the same steps for JavaScript, recording that name too, such as sandbox-javascript.

2. Install the typed client

pnpm add @sandbox-workers/core

3. Export the Sandbox Durable Object

Your own Worker — not the runtime Worker — hosts the Sandbox Durable Object. Re-export it from your entry point:

export { Sandbox } from "@sandbox-workers/core";

4. Configure your wrangler.jsonc

Add the Sandbox Durable Object binding and its migration, plus one Service Binding per runtime Worker you deployed:

{
  "durable_objects": { "bindings": [{ "name": "Sandbox", "class_name": "Sandbox" }] },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Sandbox"] }],
  "services": [
    { "binding": "PYTHON", "service": "sandbox-python" },
    { "binding": "JAVASCRIPT", "service": "sandbox-javascript" },
  ],
  "vars": { "SANDBOX_IDLE_TTL_MS": "86400000" }, // optional; "0" disables expiry
}

The service value must exactly match the name you selected when deploying, and both Workers must belong to the same Cloudflare account. The binding names (PYTHON, JAVASCRIPT) are yours to choose. Each runtime Worker needs exactly one services entry; adding a language later is one more deploy plus one more entry here, with no change to the Sandbox configuration above it.

5. Run code in two languages that share one workspace

import { getSandbox } from "@sandbox-workers/core";

export { Sandbox } from "@sandbox-workers/core";

interface Env {
  Sandbox: DurableObjectNamespace;
  PYTHON: Fetcher;
  JAVASCRIPT: Fetcher;
}

export default {
  async fetch(request: Request, env: Env) {
    const sandbox = getSandbox<Env>(env.Sandbox, "user-42");
    const py = await sandbox.interpreter.createCodeContext({ binding: "PYTHON" });
    const js = await sandbox.interpreter.createCodeContext({ binding: "JAVASCRIPT" });

    const squared = await sandbox.interpreter.runCode(
      "print('Hello!')\nimport os\nsquared = int(os.environ['X']) ** 2\nsquared",
      { context: py, envVars: { X: "12" } },
    );
    await sandbox.interpreter.runCode(
      "import json\nopen('/workspace/result.json', 'w').write(json.dumps({'squared': squared}))",
      { context: py },
    );
    const fromFile = await sandbox.interpreter.runCode(
      "fs.readFileSync('/workspace/result.json', 'utf8')",
      { context: js },
    );

    return Response.json({ squared, fromFile });
  },
};

Deploy your caller after both runtime Workers. binding: "PYTHON" and binding: "JAVASCRIPT" name the Service Bindings from step 4 — that’s what selects the language, never a language option. squared has no error, results: [{ text: "144" }], and the captured greeting in logs.stdout; fromFile reads back the file the Python context just wrote, so its result text contains 144 too.

Each context keeps its own language’s globals — py’s squared variable isn’t visible to js. But /workspace belongs to the sandbox, not to any one context, so the JavaScript context sees the file the Python context wrote there. binding selects which runtime Worker a context (or a call) talks to, and with getSandbox<Env>, it’s checked against the Service Bindings in Env at compile time, so a typo like binding: "PYTHOn" fails to build instead of failing at runtime. State in each context — top-level variables, functions, imports — persists across calls; see Use code contexts.

Add another language

Adding a third language is the same two steps again: deploy another runtime Worker from Deploy a runtime Worker, add one more services entry for it, and create a code context against that binding — it joins the same sandbox and the same /workspace as py and js above. Ruby and other stateless-only runtime Workers can’t hold code contexts (see Languages), but you can still bind one in the same caller and call it with the free runCode alongside your stateful contexts.

Next steps

  • Use code contexts: keep state between executions with a durable REPL, and use several languages in one sandbox.
  • Manage files: read and write files under /workspace.
  • Security: before exposing a caller that accepts arbitrary code, configure its authentication and rate limits.
  • API reference: request fields, responses, and error handling.

Only need one-shot execution, with no persistent state or files? See Stateless mode.