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](installshttpx), and a deployed sandbox bridge Worker. - Parameters:
api_url,api_key,python_executable,working_dir.
Deploying The Bridge​
- Create the Worker from Cloudflare's template, which deploys it and generates
the key it will accept:
It answers with a URL of the shapenpm create cloudflare -- sandbox-bridge \--template=cloudflare/sandbox-sdk/bridge/worker
https://cloudflare-sandbox-bridge.<subdomain>.workers.devand aSANDBOX_API_KEYsecret. - 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
reprcarried back on that line, which is what makesresult.textwork above. - Rich display data — a figure, an HTML repr, a PNG — has no channel at all and
is not returned.
result.resultsonly ever holds the text value. - There is no interrupt:
sandbox.interrupt()answersFalse, and a runaway execution is stopped by its timeout. - Reading a variable is refused, with the reason:
get_variablebinds 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.runis built on it: the command still runs, but its output cannot be read back, so it answers withexit_code=-1and that reason instderrrather than raising. Have the snippet print what you need, or keep it in a file. sandbox.files.readand.writedo 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 doupload_fileanddownload_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 atstart()rather than accepted and left unapplied — believing a sandbox is cut off while it is not is the failure that matters. Use thee2bvariant to cut one off, ordaytona/coreweavefor an allowlist. - There is no GPU, and
gpu=is refused for the same reason. SandboxConfig.env_varsis 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 torun_code(envs=...)wins over them.