In the 1.0 preview, treat the sandbox as a computer you drive with explicit programs.
Each exec() starts a new supervised process from argv. The call resolves when launch succeeds (you receive a process handle with id and pid properties), not when the process exits. Each launch is independent: pass cwd and env when the process needs them, or put multi-step shell syntax in one explicit shell argv. For an interactive PTY, use the terminal API.
Long-running work often spans many short Worker requests. A process ID is enough only while the same container still has that process. Across idle stop, failure, or replace, store the full launch (command, options, and any app checkpoint) so you can start again. Refer to Continue work across requests.
Most applications use one sandbox per user or task:
const sandbox = getSandbox(env.Sandbox, "user-123");const sandbox = getSandbox(env.Sandbox, "user-123");Three different things are in play:
| Term | Meaning |
|---|---|
| Sandbox ID | The stable string your app uses to find that sandbox again (for example "user-123"). |
| Container | The Containers instance currently running work for that sandbox. Sandboxes run on containers. The sandbox ID is stable. The container instance behind it is not always the same one. |
| Process | A program you start with exec() inside the current container. The handle and process.id mean “this program in this container,” not “this sandbox ID forever.” |
Same sandbox ID does not mean the same container. Processes live only in the container that started them. After a new container serves that ID, start new processes — you do not resume the old ones. Full sandbox model: Sandbox lifecycle.
The command model changes in the 1.0 preview compared with the current stable package:
| Current stable package | 1.0 preview |
|---|---|
exec(string) resolves when the command finishes |
exec(argv) resolves when the process starts |
Default session can preserve cd / export |
Each launch is independent |
startProcess / execStream for other shapes |
One process handle covers short and long-running work |
Use argv for a single binary:
const process = await sandbox.exec(["node", "--version"]);
const output = await process.output({ encoding: "utf8" });const process = await sandbox.exec(["node", "--version"]);
const output = await process.output({ encoding: "utf8" });Use an explicit shell when you need shell syntax:
const process = await sandbox.exec([
"/bin/bash",
"-lc",
"cd /workspace/app && npm test",
]);const process = await sandbox.exec([
"/bin/bash",
"-lc",
"cd /workspace/app && npm test",
]);Or pass cwd and env on the launch instead of relying on a previous command:
const process = await sandbox.exec(["npm", "test"], {
cwd: "/workspace/app",
env: { NODE_ENV: "test" },
});const process = await sandbox.exec(["npm", "test"], {
cwd: "/workspace/app",
env: { NODE_ENV: "test" },
});Sandbox-wide values use setEnvVars. Refer to Environment variables.
await sandbox.exec(argv) returns a process handle:
| Capability | Members |
|---|---|
| Identity | id, pid |
| Observe | status(), logs(), output(), waitForExit(), waitForLog(), waitForPort(), exitCode |
| Control | kill(signal?) with a numeric signal (default 15) |
Observation timeouts and AbortSignal values cancel only that wait or stream. They do not stop the process. Call kill() when you intend to stop it.
exec(argv, { timeout }) sets a remote lifetime deadline. When the supervisor stops the process for that deadline, completion can report timedOut: true.
For short commands, output() is enough. For large or long-running output, prefer logs({ since, replay, follow }) and keep the latest cursor so a later request can resume the stream while the process is still in the current container. API details: Processes API.
A process lives only as long as it keeps running in the current container for that sandbox. After the container stops, old process IDs are not valid on a later container that serves the same sandbox ID.
The container for a sandbox is not meant to run forever. After a period with nothing to do, Cloudflare may stop it. The container can also stop after failures, or when the platform replaces it during normal operations (for example after some deploys).
When that happens:
- Your app still uses the same sandbox ID (
user-123). - Processes that were running in the old container have exited. Their process IDs and live log buffers from that container are gone.
- The next time you use the sandbox for real work, Cloudflare may start a new container for the same sandbox ID. You start new processes there. You do not reconnect to process IDs from the previous container. Files from the old container are not still there unless your app restored them (for example from a backup or a mounted bucket).
Container stop and replace are not new in 1.0. The preview makes process handles fail closed after the container that owned them is gone: the SDK does not retarget an old process ID at a new container for the same sandbox ID.
| What you try | What happens |
|---|---|
| The process is still running in the current container | getProcess(id) returns it; you can read logs and wait as usual |
| No container is running for the sandbox yet | getProcess and listProcesses return null / []. They do not start a container just to answer the lookup |
| You still hold a handle from before the container stopped | Calls on that handle fail with StaleProcessHandleError |
| You need the same job after a stop | Start a new exec() from the launch and checkpoint your app stored |
Recovery procedures: Errors and recovery.
While a process or terminal is active, the container can stay running so work continues across requests. When nothing is active, the container may stop again after idle time. Long-running product flows should either keep meaningful work active or rely on checkpoints and relaunch.
Worker requests are short. Sandbox processes often are not. Design the job so a later request can either resume the same process or start the job again.
| Always useful | When you stream logs |
|---|---|
| Sandbox ID | Latest log cursor from delivered events |
Full exec argv |
|
cwd and env if the launch needs them |
|
| Application checkpoint (repo path, step, agent state) |
A process ID is a resume key for the current container only. It is not enough to restart the job after the container may have stopped.
Use this path when the work is still running and the container has not been replaced — for example another request arrives seconds later while a build or server is up.
const process = await sandbox.getProcess(storedProcessId);
if (process) {
const stream = await process.logs({
since: storedCursor,
replay: true,
follow: true,
});
// consume events; keep the latest cursor from each event
return;
}const process = await sandbox.getProcess(storedProcessId);
if (process) {
const stream = await process.logs({
since: storedCursor,
replay: true,
follow: true,
});
// consume events; keep the latest cursor from each event
return;
}You can also call status(), waitForPort(), waitForExit(), or kill() on that handle. Log cursors apply only while this process still exists in this container.
Use this path when getProcess returns null, a call throws StaleProcessHandleError, or enough time has passed that the container may have stopped or been replaced.
const process = await sandbox.exec(storedCommand, {
cwd: storedCwd,
env: storedEnv,
});
// persist process.id (and clear any old cursor)
await process.waitForPort(3000, { timeout: 60_000 });const process = await sandbox.exec(storedCommand, {
cwd: storedCwd,
env: storedEnv,
});
// persist process.id (and clear any old cursor)
await process.waitForPort(3000, { timeout: 60_000 });If the job also depends on files that lived only in the previous container, back up and restore those directories or mount durable storage before relying on the tree again. Backup and restore replace filesystem state. They do not bring back old process IDs or log buffers.
If the container is not ready yet, you may get ContainerUnavailableError — back off and run the same unit of work again. Refer to Errors and recovery.
async function continueJob(sandbox, job) {
if (job.processId) {
const existing = await sandbox.getProcess(job.processId);
if (existing) {
return existing; // Case 1 — same container, same process
}
// null: no container, or this ID is not in the current container
}
// Case 2 — relaunch from stored command and checkpoint
const process = await sandbox.exec(job.command, {
cwd: job.cwd,
env: job.env,
});
job.processId = process.id;
job.cursor = undefined;
return process;
}async function continueJob(sandbox: Sandbox, job: StoredJob) {
if (job.processId) {
const existing = await sandbox.getProcess(job.processId);
if (existing) {
return existing; // Case 1 — same container, same process
}
// null: no container, or this ID is not in the current container
}
// Case 2 — relaunch from stored command and checkpoint
const process = await sandbox.exec(job.command, {
cwd: job.cwd,
env: job.env,
});
job.processId = process.id;
job.cursor = undefined;
return process;
}If you still hold a handle object from before the container stopped, calls on that handle throw StaleProcessHandleError. Prefer getProcess(id) on each new request instead of reusing an old handle across requests.
Process (exec) |
Terminal | |
|---|---|---|
| Role | Supervised argv process | Interactive PTY |
| Input | Launch-time argv | PTY input (write / browser connect) |
| Stop | kill(signal?) |
interrupt() / terminate() |
| Lookup | getProcess / listProcesses |
getTerminal / listTerminals |
Both follow the same container lifetime rules. Terminal docs: Terminals. API: Terminals API.
output() buffers stdout and stderr and may set truncated: true. Prefer logs() when output may be large or the process runs longer than one Worker request.
const stream = await process.logs({ follow: true, replay: true });
// each data/terminal event includes a cursor — store the latestconst stream = await process.logs({ follow: true, replay: true });
// each data/terminal event includes a cursor — store the latestOn a later request against the same still-running container, call getProcess(id) and resume with logs({ since: cursor, replay: true, follow: true }). After a new container starts for the sandbox, start a new process; the old cursor does not apply.
Event shapes, wait options, and readiness checks: Processes API.