Skip to main content

Cloudflare

Runs code in a Cloudflare Sandbox — a container on Cloudflare's edge, started next to the Worker that owns it.

Cloudflare's own SDK is a Workers binding written in TypeScript, and a Python process cannot hold one: getSandbox(env.Sandbox, id) only means something inside a Worker. What a Python process can talk to is the sandbox bridge, a small reference-implementation Worker Cloudflare publishes that exposes the SDK over HTTP, and that is what this variant drives. It is deployed once per account; see below.

State does not persist between calls. The bridge's exec starts a process and streams its output back, and gives nothing to write to its stdin, so a namespace cannot be held open between snippets the way the CoreWeave and Modal variants hold one — each snippet runs in a process of its own:

sandbox.run_code("x = 40")
sandbox.run_code("x + 2").code_error.name # "NameError"
  • Requirements: code-sandboxes[cloudflare] (installs httpx), and a deployed sandbox bridge Worker.
  • Parameters: api_url, api_key, python_executable, working_dir.

Deploying The Bridge​

  1. Create the Worker from Cloudflare's template, which deploys it and generates the key it will accept:
    npm create cloudflare -- sandbox-bridge \
    --template=cloudflare/sandbox-sdk/bridge/worker
    It answers with a URL of the shape https://cloudflare-sandbox-bridge.<subdomain>.workers.dev and a SANDBOX_API_KEY secret.
  2. Export the URL, which is required, and the key, which every bridge deployed with one requires — a bridge on the public internet with no key is a sandbox anybody may run code in, and only a local development bridge should have none. The provider is listed as available only when both are set:
    export CLOUDFLARE_SANDBOX_API_URL="https://cloudflare-sandbox-bridge.example.workers.dev"
    export CLOUDFLARE_SANDBOX_API_KEY="..."

Either can be passed to the sandbox instead of exported:

Sandbox.create(variant="cloudflare", api_url="https://...workers.dev", api_key="...")

The key travels as a bearer token. A bridge that refuses it says so with the reason rather than with a bare HTTP code — a 401 means the key is wrong, and the message names the variable to check.

The bridge documentation is at developers.cloudflare.com/sandbox/bridge.

Usage​

from code_sandboxes import Sandbox

with Sandbox.create(variant="cloudflare") as sandbox:
result = sandbox.run_code("import sys; sys.version_info[:2]")
print(result.text) # "(3, 11)"

Snippets run in /workspace unless working_dir= says otherwise.

Carrying State Between Calls​

Since each snippet is its own process, anything that has to survive a call has to be written down somewhere both processes can see. There are two ways, and neither is a workaround for a bug — it is what a stateless runner means.

Put the statements that share state in one snippet:

sandbox.run_code("""
x = 40
x + 2
""").text # "42"

Or keep the state in a file. The filesystem of the sandbox does persist between calls:

sandbox.files.write("/workspace/state.json", '{"x": 40}')
sandbox.run_code(
"import json; json.load(open('/workspace/state.json'))['x'] + 2"
).text # "42"

The same fact is what set_variable refuses to pretend about: a variable set from outside would be gone before the next snippet could read it, so the call explains that instead of quietly doing nothing.

Files​

Files go straight to the bridge's filesystem endpoints rather than through a program that decodes them, so a large file does not have to become a large snippet:

sandbox.files.write_bytes("/workspace/data.parquet", payload)
sandbox.files.read_bytes("/workspace/data.parquet")

Management​

create, get and delete are answered; list is not. The bridge exposes a sandbox by its id and has no endpoint that enumerates them, so code-sandboxes list -v cloudflare raises with that reason rather than answering with an empty list — "none" and "cannot know" are different facts, and a management tool that confuses them loses sandboxes. See the management guide.

What Is Not Reported​

Each snippet is run by a small program that captures the streams and answers with one JSON line, so:

  • The value of a trailing expression is reported — it is evaluated and its repr carried back on that line, which is what makes result.text work above.
  • Rich display data — a figure, an HTML repr, a PNG — has no channel at all and is not returned. result.results only ever holds the text value.
  • There is no interrupt: sandbox.interrupt() answers False, and a runaway execution is stopped by its timeout.
  • Reading a variable is refused, with the reason: get_variable binds the value in one execution and reads it back in a second, and this provider's first process is gone before the second starts. sandbox.commands.run is built on it: the command still runs, but its output cannot be read back, so it answers with exit_code=-1 and that reason in stderr rather than raising. Have the snippet print what you need, or keep it in a file.
  • sandbox.files.read and .write do work: they go through the bridge's own file endpoints rather than through a variable, which is one round trip and needs no session. So do upload_file and download_file.
  • The network cannot be restricted. The bridge exposes no egress rules, no allowlist and no switch, so network_policy="none" or "allowlist" is refused at start() rather than accepted and left unapplied — believing a sandbox is cut off while it is not is the failure that matters. Use the e2b variant to cut one off, or daytona / coreweave for an allowlist.
  • There is no GPU, and gpu= is refused for the same reason.
  • SandboxConfig.env_vars is honoured: the bridge takes no environment when it creates a sandbox, so the configured variables are set at the top of every snippet instead. Anything passed to run_code(envs=...) wins over them.