Durable Object Container API (2)·续1

  • Promise<ExecProcess>: Resolves with an ExecProcess for the started process.

An ExecProcess has these fields and methods:

  • stdin ( WritableStream | undefined): Writable standard input when stdin is "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. ExecOutput contains stdout ( ArrayBuffer), stderr ( ArrayBuffer), and exitCode ( number). Ignored streams produce empty buffers. Use TextDecoder to decode text.
  • kill(signal?: number) ( void): Queues a signal for the process. The default is SIGTERM, signal 15. The signal must be from 1 through 64.
  • resize(cols: number, rows: number) ( void): Changes the pseudo-terminal size. Both values must be from 1 through 65535.

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 a TypeError when cmd is empty, an option mode is invalid, stderr: "combined" is used with stdout: "ignore", or pty is used with a stderr value other than "combined".
  • exec() throws an AbortError when signal is already aborted.
  • exec() rejects if the runtime cannot create or start the process.
  • Environment variable names cannot contain = or null characters. Environment values, cwd, and user cannot contain null characters.
  • kill() throws a RangeError when the signal is outside the supported range.
  • resize() throws a TypeError when the process does not have a pseudo-terminal, and a RangeError when 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 returned ContainerSnapshot to start() as containerSnapshot, or store it for a later restore.
  • size ( number): Snapshot size in bytes.
  • name ( string, optional): Human-readable name supplied in options.

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 an Error when the container is not running.
  • snapshotContainer() throws a TypeError when options is 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 as SIGTERM ( 15) or SIGKILL ( 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 than 0 and at most 6 hours ( 21600000).

Return values

  • Promise<void>: Resolves after the timeout is set.

Exceptions

  • setInactivityTimeout() throws a TypeError when durationMs is 0 or less.
  • The returned promise rejects when durationMs is 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 });
}