running forever is one where nothing can ever be safely
reclaimed — and the disk fills with workspaces nobody can account for.
Two deadlines fix that, and both are on by default. Each is enforced twice: by the sandbox itself,
and by a sweep in your process.
Neither deletes anything. A stopped sandbox restarts on the next
create() with everything the
last command installed still there; a retired one gets a fresh container over the same warm
workspace. Only remove() deletes.
Per instance, or per sandbox
Set the default for a fleet on the constructor, and override it for one sandbox that needs different treatment:0 disables a deadline, per sandbox or per instance.
The deadlines live on the container
They are recorded as labels rather than held in the process. That means the sweep honours them after a restart, and any other process reading the fleet sees the same clocks — a sandbox created by a worker that has since exited is still retired on time, by whoever is sweeping.A command in flight is never interrupted
Neither deadline touches a sandbox with a command running. The maximum lifetime is a ceiling on how long a sandbox can be reused, not a kill switch under a running build. A three-hour build in a sandbox with a one-hour ceiling finishes; it is retired on the next sweep afterwards.The sandbox stops itself
Every sandbox’s main process is a small watchdog (sh, date, sleep and /proc, so it runs in
busybox and glibc images alike). Every few seconds it looks for any other process in the sandbox:
- Something is running — a command, or anything a command left in the background — and the idle clock resets.
- Nothing has run for
idleTimeoutMs, and it exits. The container stops. - At
maxLifetimeMsplusidleTimeoutMsit exits whatever is running: a backstop behind the sweep’s gentler retirement below, which lets a command in flight finish.
SIGTERM, so stop() returns at once rather than waiting out its grace.
selfStop: false gives a sandbox sleep infinity instead, and leaves stopping it entirely to the
sweep. list() tells the two apart through the nativesandbox.selfStop label.
Sweeping
A timer runs both passes automatically, at a fraction of the soonest deadline so a sandbox is stopped within — not up to double — the interval asked for. It is unref’d, so it will never keep your process alive on its own.housekeeping: false does not start it — for a short-lived CLI, or a
process that should never stop sandboxes it did not create. Sandboxes still stop themselves.
Run them yourself when you want control:
now for testing:
sandboxes.close() at shutdown to stop the timer.
A sandbox this process has never seen
One left running by a previous process is counted as used from the moment it is first seen, so it is stopped one full timeout later rather than immediately and rather than never. A restart does not mass-stop a working fleet, and it does not forget about it either.Choosing the numbers
The cost of stopping too eagerly is a coldnpm install on the next command — exactly what the
reuse rule exists to avoid. The cost of stopping too late is a container holding memory it is
not using.
Five minutes spans a burst of activity from one user without keeping an abandoned sandbox
“running” by the time anyone looks. Raise it if your work arrives in widely spaced bursts;
raising it is the safe direction, because a stopped sandbox keeps its cache anyway.