Durable Object Container API (2)·续1
Promise<ExecProcess>: Resolves with anExecProcessfor the started process.
An ExecProcess has these fields and methods:
stdin(WritableStream | undefined): Writable standard input whenstdinis"pipe".stdout(ReadableStream | undefined): Readable standard output when piped.stderr(ReadableStream | undefined): Readable standard error when piped separately.pid(number): Process identifier.isPty(boolean): Whether the process runs attached to a pseudo-terminal.exitCode(Promise<number>): Resolves when the process exits. Nonzero codes resolve normally instead of rejecting.output()(Promise<ExecOutput>): Reads buffered output once.ExecOutputcontainsstdout(ArrayBuffer),stderr(ArrayBuffer), andexitCode(number). Ignored streams produce empty buffers. UseTextDecoderto decode text.kill(signal?: number)(void): Queues a signal for the process. The default isSIGTERM, signal15. The signal must be from1through64.resize(cols: number, rows: number)(void): Changes the pseudo-terminal size. Both values must be from1through65535.
A stream that the process does not provide is undefined. For example, with stderr: "combined" or pty, stderr is undefined on ExecProcess and an empty ArrayBuffer on ExecOutput. Read both output channels from stdout.
The Workers TypeScript types declare stdin, stdout, and stderr as | null. Test for a stream, such as if (process.stderr), instead of comparing with null.
output() throws a TypeError when called more than once or after either readable stream starts being consumed.
output() holds all of the output in the memory of the Durable Object, where it counts toward the memory limit. When output can be large, read both streams concurrently instead. For more information, refer to Stream large output.
Process lifetime
exec() has no built-in timeout. Use kill() to request termination, then observe completion through exitCode. A process can handle or ignore a signal, so this does not enforce a hard deadline. Do not infer a specific exit code from the signal.
kill() and signal reach only the process that exec() started. Processes that this process starts, such as the commands in a bash -c script, keep running. output() waits until they exit. To stop them too, refer to Stop the processes a command starts.
Canceling the request that started a process does not stop the process. If the client retries, a second copy of the command runs.
To stop the process when the client disconnects, turn on the enable_request_signal compatibility flag in the Worker that defines the Durable Object, and call the Durable Object through fetch() on its stub. Then abort the exec() signal when request.signal aborts. RPC methods do not receive a request signal.
export class MyContainer extends DurableObject {
async fetch(request) {
const container = this.ctx.container;
if (!container?.running) {
return new Response("The container is not running", { status: 503 });
}
const controller = new AbortController();
const abort = () => controller.abort();
if (request.signal.aborted) {
abort();
} else {
request.signal.addEventListener("abort", abort, { once: true });
}
const process = await container.exec(["sleep", "60"], {
signal: controller.signal,
});
// Stop forwarding the abort once the process exits.
void process.exitCode.then(() =>
request.signal.removeEventListener("abort", abort),
);
const output = await process.output();
return new Response(`Exit code: ${output.exitCode}`);
}
}
export class MyContainer extends DurableObject {
async fetch(request: Request) {
const container = this.ctx.container;
if (!container?.running) {
return new Response("The container is not running", { status: 503 });
}
const controller = new AbortController();
const abort = () => controller.abort();
if (request.signal.aborted) {
abort();
} else {
request.signal.addEventListener("abort", abort, { once: true });
}
const process = await container.exec(["sleep", "60"], {
signal: controller.signal,
});
// Stop forwarding the abort once the process exits.
void process.exitCode.then(() =>
request.signal.removeEventListener("abort", abort),
);
const output = await process.output();
return new Response(`Exit code: ${output.exitCode}`);
}
}
Caution
Signal only a process that is still running. Aborting signal or calling kill() after the process exits raises an uncaught internal error in the Durable Object.
Do not pass AbortSignal.timeout() or request.signal to exec() directly, because either can fire after the process exits. Pass the signal of an AbortController instead. Once exitCode resolves, stop anything that calls abort(), for example by clearing the timer or removing the abort listener from request.signal.
Exceptions
exec()throws when the container is not running.exec()throws aTypeErrorwhencmdis empty, an option mode is invalid,stderr: "combined"is used withstdout: "ignore", orptyis used with astderrvalue other than"combined".exec()throws anAbortErrorwhensignalis already aborted.exec()rejects if the runtime cannot create or start the process.- Environment variable names cannot contain
=or null characters. Environment values,cwd, andusercannot contain null characters. kill()throws aRangeErrorwhen the signal is outside the supported range.resize()throws aTypeErrorwhen the process does not have a pseudo-terminal, and aRangeErrorwhen a dimension is out of range.
For task-oriented examples, refer to Execute commands.
snapshotContainer durable_object policy only
snapshotContainer() creates a point-in-time snapshot of the writable root filesystem of a running container. It does not capture memory, running processes, or separately mounted filesystems. A container started from the snapshot runs its entrypoint again. This method is only supported for applications that use the durable_object scheduling policy.
const snapshot = await this.ctx.container.snapshotContainer({
name: "before-upgrade",
});
Note
The Worker API has no method to list snapshots. Store the returned id, for example in Durable Object storage.
Parameters
options(ContainerSnapshotOptions): Snapshot configuration. Pass{}when you do not set a name:name(string, optional): Human-readable name for the snapshot.
Return values
Promise<ContainerSnapshot>: Resolves with an opaque handle for the stored filesystem snapshot. The snapshot data is not returned to the Worker. The handle contains:id(string): Unique snapshot identifier. Pass the returnedContainerSnapshottostart()ascontainerSnapshot, or store it for a later restore.size(number): Snapshot size in bytes.name(string, optional): Human-readable name supplied inoptions.
Container snapshots are immutable. Snapshot handles have an implicit 30-day time-to-live that refreshes when you restore them. For the complete save and restore flow, refer to Use snapshots.
Exceptions
snapshotContainer()throws anErrorwhen the container is not running.snapshotContainer()throws aTypeErrorwhenoptionsis omitted.
destroy
destroy() stops the container and can include an optional reason for the operation.
await this.ctx.container.destroy("Manually Destroyed");
Parameters
error(any, optional): Optional reason associated with the destroy operation. A string is commonly used for logging or debugging.
Return values
Promise<void>: Resolves when the container is destroyed.
signal
signal() sends an inter-process communication (IPC) signal to the container, such as SIGKILL or SIGTERM. Use it to stop the container gracefully or forcefully.
const SIGTERM = 15;
this.ctx.container.signal(SIGTERM);
Parameters
signal(number): POSIX signal number to send to the container, such asSIGTERM(15) orSIGKILL(9).
Return values
void: No return value.
setInactivityTimeout
setInactivityTimeout() sets how long a container keeps running after its Durable Object becomes inactive. If the Durable Object handles another request before the timeout ends, the same container is still running. Otherwise, Cloudflare stops the container. Without a timeout, Cloudflare stops the container shortly after the Durable Object becomes inactive.
Each Durable Object instance sets its own timeout. A Durable Object that restarts, for example after a deploy, starts without one. Set the timeout after start(), and set it again in the constructor when the container is already running.
setInactivityTimeout(durationMs: number | bigint): Promise<void>
index.jsjs
import { DurableObject } from "cloudflare:workers";
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyDurableObject extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
const container = ctx.container;
if (container?.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async ensureRunning() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
container.start({
// `cloudflare/debian-trixie` requires the `durable_object` scheduling policy.
image: "cloudflare/debian-trixie",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
}
index.tsts
import { DurableObject } from "cloudflare:workers";
interface Env {}
const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
const container = ctx.container;
if (container?.running) {
void ctx.blockConcurrencyWhile(() =>
container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS),
);
}
}
async ensureRunning() {
const container = this.ctx.container;
if (!container) {
throw new Error("No container is configured for this Durable Object");
}
if (!container.running) {
container.start({
// `cloudflare/debian-trixie` requires the `durable_object` scheduling policy.
image: "cloudflare/debian-trixie",
entrypoint: ["sleep", "infinity"],
enableInternet: false,
});
await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS);
}
}
}
Parameters
durationMs(number | bigint): Inactivity timeout in milliseconds. The value must be greater than0and at most 6 hours (21600000).
Return values
Promise<void>: Resolves after the timeout is set.
Exceptions
setInactivityTimeout()throws aTypeErrorwhendurationMsis0or less.- The returned promise rejects when
durationMsis greater than 6 hours.
Await the call inside a try block to handle both.
getTcpPort
getTcpPort() returns a TCP port from the container. Use it to communicate with the container over TCP or HTTP.
const port = this.ctx.container.getTcpPort(8080);
const res = await port.fetch("http://container/set-state", {
body: initialState,
method: "POST",
});
const conn = this.ctx.container.getTcpPort(8080).connect("10.0.0.1:8080");
await conn.opened;
try {
if (request.body) {
await request.body.pipeTo(conn.writable);
}
return new Response(conn.readable);
} catch (error) {
console.error("Request body piping failed:", error);
return new Response("Failed to proxy request body", { status: 502 });
}
