Debugging OpenSandbox Sandboxes
Companion to OpenSandbox Best Practices. That
page says what to configure; this one says how to check what you actually got. Everything in the
first two sections runs inside the sandbox through sandbox.exec() (or a bash tool call in the
agent) — no cluster access needed. Sandboxes run on cgroup v2 with /sys/fs/cgroup mounted
read-only, so the files below are always readable.
What am I getting? The enforced limits
If cpu.max says 4 CPUs but nproc says 192, this cluster does not overlay CPU counts and the
host-core fan-out trap
applies.
How much am I using?
Throttling ratio = throttled_usec / (usage_usec + throttled_usec). A few percent means the CPU
limit is binding; above ~30% your commands are running dramatically slower than they look.
Did I hit the ceiling?
memory.peak == memory.max with a large max count means you are living at the ceiling even if
nothing has died yet. oom_kill > 0 with the sandbox still alive means a child process was killed.
If the whole sandbox died you will not get to read this file — see
detecting OOM from the client.
One-shot snapshot for the end of a rollout
Capture this into your rollout record and you can size limits from real peaks across a whole run instead of from one failure message.
Detecting OOM from the client
An OOM has one of two shapes, depending on what the kernel killed.
A child process died, the sandbox survived
From the client this is an ordinary command failure:
exec()returns normally withreturn_code == 137(or-9), usuallyKilledin stderr and truncated output.- Confirm from inside:
memory.eventsshowsoom_kill > 0, andmemory.peakis at or nearmemory.max. - The sandbox is fine to keep using; the agent can retry a smaller step.
The whole sandbox died
Kubernetes group-OOM kills the entire cgroup, so the next operation cannot reach the exec daemon. The provider recognises this, polls the sandbox status for up to 5 seconds to learn why, and raises a typed error with the reason attached:
Afterwards await sandbox.status() returns SandboxStatus.ERROR or SandboxStatus.STOPPED — that
is the provider-neutral check if you do not want to import the provider’s exception type. The same
notice surfaces on the PTY path as
SandboxPtyError("PTY attach takeover kept being refused: Sandbox was OOM-killed. ...").
Do not confuse OOM with a timeout. A command that exceeds its timeout_s raises TimeoutError; a
wedged command raises TimeoutError mentioning the hard cap. Neither is an OOM — when in doubt,
read memory.events before deciding.
Counting OOMs across a run
Rollout records carry sandbox observations under ng_agent_observations: each record has
outcome (completed, failed, timeout, sandbox_error), exit_code, and error_type.
outcome: sandbox_error with error_type: SandboxBackendUnreachableError, or exit_code: 137, is
your OOM census. Join it on the task id: OOMs clustered on a few tasks mean fan-out or runaway code
in those repos; OOMs spread evenly mean the limit is genuinely too small.
Cluster-side checks
If you have kubectl access to the sandbox namespace. The pod name is the sandbox id the SDK
returned with -0 appended; sandboxes also carry the opensandbox.io/id label.
The resources jsonpath is the fastest way to confirm you did not accidentally set the same map
for requests and limits.