Daytona
Runs code in a Daytona sandbox — an isolated cloud sandbox that starts in well under a second.
State persists between calls. The variant drives the sandbox's code
interpreter (sandbox.code_interpreter), which holds one Python namespace per
context, rather than process.code_run, which is a fresh process per snippet:
sandbox.run_code("x = 40")
sandbox.run_code("x + 2").text # "42"
- Requirements:
code-sandboxes[daytona](installsdaytona). - Parameters:
api_key,api_url,target,jwt_token,organization_id,snapshot,image,python_version,gpu_count,spot,delete_on_stop.
How To Obtain Daytona Credentials​
- Sign in at app.daytona.io.
- Create an API key and export it:
export DAYTONA_API_KEY="dtn_..."
- Optionally point at another deployment or region:
export DAYTONA_API_URL="https://app.daytona.io/api" # the defaultexport DAYTONA_TARGET="eu" # your default region otherwise
JWT authentication works too, and then the organization has to be named
alongside it: DAYTONA_JWT_TOKEN with DAYTONA_ORGANIZATION_ID.
Every one of these can be passed to the sandbox instead of exported. What is
left out is read from the environment by the Daytona SDK, so passing nothing is
not the same as passing None:
Sandbox.create(variant="daytona", api_key="dtn_...", target="eu")
Usage​
from code_sandboxes import Sandbox
with Sandbox.create(variant="daytona") as sandbox:
result = sandbox.run_code("import numpy as np; np.arange(5).sum()")
print(result.text) # "10"
Contexts​
The default namespace is shared by every call. create_context() asks Daytona
for one it keeps apart, so two pieces of work can run in the same sandbox
without seeing each other's variables:
with Sandbox.create(variant="daytona") as sandbox:
other = sandbox.create_context()
sandbox.run_code("secret = 1")
sandbox.run_code("secret", context=other).code_error.name # "NameError"
GPUs​
Daytona's own flavours — H100, H200, RTX-PRO-6000, RTX-4090,
RTX-5090. A name Daytona does not have is refused before any sandbox is
created, naming the ones it does:
Sandbox.create(variant="daytona", gpu="H100")
Sandbox.create(variant="daytona", gpu="H100", gpu_count=2)
Name several, comma-separated, to say which you would prefer. Daytona takes the first of them it can find, so a workload that runs on either does not fail because the better card is busy:
Sandbox.create(variant="daytona", gpu="H100,H200,RTX-4090")
A GPU — like any request for cpu or memory — is a machine specification,
and Daytona accepts one only when the sandbox is built from an image. So
asking for resources builds from Image.debian_slim() instead of starting from
the default snapshot, and takes longer to come up. Pass image= to choose that
image yourself, or snapshot= to start from a snapshot of your organization
when you need no specification.
Spot GPUs​
spot=True runs on preemptible capacity: far cheaper than on-demand, and
outside your organization's GPU quota — available capacity is the only limit.
The price is that Daytona can take the sandbox back at any moment, without
warning, when on-demand capacity needs it.
Sandbox.create(variant="daytona", gpu="H100,H200", spot=True)
Spot is GPU-only and is built from an image rather than from a snapshot. Both
are checked here: spot=True without a gpu=, or together with a snapshot=,
is refused with the reason rather than coming back as an API error.
Every GPU sandbox is created as ephemeral — auto_delete_interval=0, so it
is deleted rather than kept when it stops. That is Daytona's rule, not this
package's: it refuses any other value with "GPU sandboxes must be ephemeral".
It applies to on-demand GPUs as much as to preemptible ones, so a GPU sandbox
cannot be created detached and picked up later the way a plain one can.
An ordered list of GPUs is worth more on spot than anywhere else — what is free changes minute to minute.
Surviving preemption​
There is no warning and no webhook. What there is, is a timestamp: a reclaimed sandbox is marked and stays retrievable for 24 hours. Ask it directly:
if sandbox.preempted_at():
print("reclaimed, start again elsewhere")
You rarely have to. A reclaimed sandbox fails the way a dropped connection
does, so run_code asks on your behalf and says which it was:
result = sandbox.run_code("train()")
if not result.execution_ok:
print(result.execution_error)
# "The spot sandbox was reclaimed at 2026-08-21T10:00:00Z: preemptible
# capacity is taken back when on-demand capacity needs it. ..."
The question costs a round trip, so it is only asked of a sandbox that could have been reclaimed at all.
Falling back to on-demand​
When there is no spot capacity, creation fails immediately — there is no queue to wait in. Falling back is left to you, deliberately: on-demand costs more and counts against your quota, and that is not a decision to make quietly on your behalf.
try:
sandbox = Sandbox.create(variant="daytona", gpu="H100", spot=True)
except Exception:
sandbox = Sandbox.create(variant="daytona", gpu="H100")
Network policy​
The policy of the configuration becomes Daytona's own network settings:
# No outbound network at all.
Sandbox.create(variant="daytona", network_policy="none")
# Only these domains.
Sandbox.create(
variant="daytona",
network_policy="allowlist",
allowed_hosts=["pypi.org", "files.pythonhosted.org"],
)
Lifecycle​
Leaving the with block deletes the sandbox. Daytona sandboxes otherwise
outlive the program that made them, and one nobody deletes goes on costing
storage. Pass delete_on_stop=False to stop it instead, leaving it in the
organization to be started again:
with Sandbox.create(variant="daytona", delete_on_stop=False) as sandbox:
sandbox.run_code("open('/tmp/work', 'w').write('kept')")
code-sandboxes list -v daytona shows what is there; see the
management guide.
Names and tags​
A Daytona name is an address — daytona.get(name) — and has to be unique
within an organization, while the names this package generates are meant to be
readable and may repeat. So the name and any tags travel as labels, next to
created-by=code-sandboxes, which is what tells the sandboxes this package
made from the rest of the organization's:
Sandbox.create(variant="daytona", name="nightly-report", tags={"team": "ai"})
What Is Not Reported​
The interpreter answers with stdout, stderr, and the error when the code raised. There is no execute_result on the wire, so:
- The value of a trailing expression is captured for you — it is evaluated,
bound, and its
reprcarried back on a marked line of stdout that never reaches your logs. That 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.
Variables cross as JSON (get_variable, set_variable, and everything built
on them such as sandbox.commands.run), so a value that cannot be encoded
comes back as its repr rather than as the object itself, and one that cannot
be sent is refused with a reason.
Binary files go straight to Daytona's filesystem API rather than through a program that decodes them:
sandbox.files.write_bytes("/tmp/data.parquet", payload)
sandbox.files.read_bytes("/tmp/data.parquet")