Container Interface
The Container class ↗ from @cloudflare/containers ↗ is the standard way to interact with container instances from a Worker. It wraps the underlying Durable Object interface and provides a higher-level API for common container behaviors.
npm i @cloudflare/containers yarn add @cloudflare/containers pnpm add @cloudflare/containers bun add @cloudflare/containers Then, define a class that extends Container and set the shared properties on the class:
import { Container, getContainer } from "@cloudflare/containers";
export class SandboxContainer extends Container { defaultPort = 8080; requiredPorts = [8080, 9222]; sleepAfter = "5m"; envVars = { NODE_ENV: "production", LOG_LEVEL: "info", }; entrypoint = ["npm", "run", "start"]; enableInternet = false; pingEndpoint = "localhost/ready";}
export default { async fetch(request, env) { return getContainer(env.SANDBOX_CONTAINER, "workspace-123").fetch(request); },};import { Container, getContainer } from "@cloudflare/containers";
export class SandboxContainer extends Container { defaultPort = 8080; requiredPorts = [8080, 9222]; sleepAfter = "5m"; envVars = { NODE_ENV: "production", LOG_LEVEL: "info", }; entrypoint = ["npm", "run", "start"]; enableInternet = false; pingEndpoint = "localhost/ready";}
export default { async fetch(request: Request, env) { return getContainer(env.SANDBOX_CONTAINER, "workspace-123").fetch(request); },};The Container class extends DurableObject, so all Durable Object functionality is available.
Configure these as class fields on your subclass. They apply to every instance of the container.
-
defaultPort(number, optional) — the port your container process listens on.fetch()andcontainerFetch()forward requests here unless you specify a different port viaswitchPort()or theportargument tocontainerFetch(). Most subclasses set this. -
requiredPorts(number[], optional) — ports that must be accepting connections before the container is considered ready. Used bystartAndWaitForPorts()when noportsargument is passed. Set this when your container runs multiple services that all need to be healthy before serving traffic. -
sleepAfter(string | number, default:"10m") — how long to keep the container alive without activity before shutting it down. Accepts a number of seconds or a duration string such as"30s","5m", or"1h". Activity resets the timer — seerenewActivityTimeout()for manual resets. -
envVars(Record<string, string>, default:{}) — environment variables passed to the container on every start. For per-instance variables, passenvVarsthroughstartAndWaitForPorts()instead. -
entrypoint(string[], optional) — overrides the image's default entrypoint. Useful when you want to run a different command without rebuilding the image, such as a dev server or a one-off task. -
enableInternet(boolean, default:true) — controls whether the container can make outbound HTTP requests. Set tofalsefor sandboxed environments where you want to intercept or block all outbound traffic. For more information, refer to Handle outbound traffic. -
pingEndpoint(string, default:"ping") — the host and path the class uses to health-check the container during startup. Most users do not need to change this.
Override these methods to run Worker code when the container changes state. Refer to the status hooks example for a full example.
Run Worker code after the container has started.
onStart(): void | Promise<void>Returns: void | Promise<void>. Resolve after any startup logic finishes.
Use this to log startup, seed data, or schedule recurring tasks with schedule().
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async onStart() { await this.containerFetch("http://localhost/bootstrap", { method: "POST", }); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
override async onStart() { await this.containerFetch("http://localhost/bootstrap", { method: "POST", }); }
}Run Worker code after the container process exits.
onStop(params: StopParams): void | Promise<void>Parameters:
params.exitCode- Container process exit code.params.reason- Why the container stopped:'exit'when the process exited on its own, or'runtime_signal'when the runtime signalled it.
Returns: void | Promise<void>. Resolve after your shutdown logic finishes.
Use this to log, alert, or restart the container.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { onStop({ exitCode, reason }) { console.log("Container stopped", { exitCode, reason }); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { override onStop({ exitCode, reason }) { console.log("Container stopped", { exitCode, reason }); }}Handle startup and port-checking errors.
onError(error: unknown): anyParameters:
error- The error thrown during startup or port checks.
Returns: any. The default implementation logs the error and re-throws it.
Override this to suppress errors, notify an external service, or attempt a restart.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { onError(error) { console.error("Container failed to start", error); throw error; }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { override onError(error: unknown) { console.error("Container failed to start", error); throw error; }}Run Worker code when the sleepAfter timer expires.
onActivityExpired(): Promise<void>Returns: Promise<void>. Resolve after your idle-time logic finishes.
Called when the sleepAfter timeout expires with no incoming requests. The default implementation calls stop().
If you override this method without stopping the container, the timer renews and the hook fires again on the next expiry.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { sleepAfter = "2m";
async onActivityExpired() { const state = await this.getState(); console.log("Container is idle, stopping it now", state.status);
await this.stop(); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { sleepAfter = "2m";
override async onActivityExpired() { const state = await this.getState(); console.log("Container is idle, stopping it now", state.status);
await this.stop(); }
}Handle incoming HTTP or WebSocket requests.
fetch(request: Request): Promise<Response>Parameters:
request- The incoming request to proxy to the container.
Returns: Promise<Response> from the container or from your custom routing logic.
By default, fetch forwards the request to the container process at defaultPort. The container is started automatically if it is not already running.
Override fetch when you need routing logic, authentication, or other middleware before forwarding to the container. Inside the override, call this.containerFetch() rather than this.fetch() to avoid infinite recursion:
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async fetch(request) { const url = new URL(request.url);
if (url.pathname === "/health") { return new Response("ok"); }
return this.containerFetch(request); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
override async fetch(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname === "/health") { return new Response("ok"); }
return this.containerFetch(request); }
}fetch is the only method that supports WebSocket proxying. Refer to the WebSocket example for a full example.
Send an HTTP request directly to the container process. Generally, users should prefer
to use fetch unless it has been overrriden.
containerFetch(request: Request, port?: number): Promise<Response>containerFetch(url: string | URL, init?: RequestInit, port?: number): Promise<Response>Parameters:
request- ExistingRequestobject to forward.url- URL to request when you are constructing a new request.init- StandardRequestInitoptions for the URL-based overload.port- Optional target port. If omitted, the class usesdefaultPort.
Returns: Promise<Response> from the container.
This is what the default fetch() implementation calls internally, and it is what you should call from within an overridden fetch() method to avoid infinite recursion. It also accepts a standard fetch-style signature with a URL string and RequestInit, which is useful when you are constructing a new request rather than forwarding an existing one.
Does not support WebSockets. Use fetch() with switchPort() for those.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async fetch(request) { const url = new URL(request.url);
if (url.pathname === "/metrics") { return this.containerFetch( "http://localhost/internal/metrics", { headers: { authorization: request.headers.get("authorization") ?? "", }, }, 9090, ); }
return this.containerFetch(request); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
override async fetch(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname === "/metrics") { return this.containerFetch( "http://localhost/internal/metrics", { headers: { authorization: request.headers.get("authorization") ?? "", }, }, 9090, ); }
return this.containerFetch(request); }
}In most cases you do not need to call these methods directly. fetch() and containerFetch() start the container automatically. Call these explicitly when you need to pre-warm a container, run a task on a schedule, or control the lifecycle from within a lifecycle hook.
Start the container and wait until the target ports are accepting connections.
startAndWaitForPorts(args?: StartAndWaitForPortsOptions): Promise<void>startAndWaitForPorts( ports?: number | number[], cancellationOptions?: CancellationOptions, startOptions?: ContainerStartConfigOptions,): Promise<void>Parameters:
args.ports- Port or ports to wait for. Port resolution order is explicitports, thenrequiredPorts, thendefaultPort.args.startOptions- Per-instance startup overrides.args.startOptions.envVars- Per-instance environment variables.args.startOptions.entrypoint- Entrypoint override for this start only.args.startOptions.enableInternet- Whether outbound internet access is allowed for this start.args.cancellationOptions.abort- Abort signal to cancel startup.args.cancellationOptions.instanceGetTimeoutMS- Maximum time to get a container instance and issue the start command. Default:8000.args.cancellationOptions.portReadyTimeoutMS- Maximum time to wait for all ports to become ready. Default:20000.args.cancellationOptions.waitInterval- Polling interval in milliseconds. Default:300.
Returns: Promise<void>. Resolves after the target ports are ready and onStart() has run.
This is the safest way to explicitly start a container when you need to be certain it is ready before sending traffic.
This method also supports positional ports, cancellationOptions, and startOptions arguments, but the object form is easier to read.
import { getContainer } from "@cloudflare/containers";
export default { async scheduled(_event, env) { const container = getContainer(env.API_CONTAINER, "tenant-42");
await container.startAndWaitForPorts({ ports: [8080, 9222], startOptions: { envVars: { API_KEY: env.API_KEY, TENANT_ID: "tenant-42", }, }, cancellationOptions: { portReadyTimeoutMS: 30_000, }, }); },};import { getContainer } from "@cloudflare/containers";
export default { async scheduled(_event, env) { const container = getContainer(env.API_CONTAINER, "tenant-42");
await container.startAndWaitForPorts({ ports: [8080, 9222], startOptions: { envVars: { API_KEY: env.API_KEY, TENANT_ID: "tenant-42", }, }, cancellationOptions: { portReadyTimeoutMS: 30_000, }, }); },
};Refer to the env vars and secrets example for a full example.
Start the container without waiting for all ports to become ready.
start(startOptions?: ContainerStartConfigOptions, waitOptions?: WaitOptions): Promise<void>Parameters:
startOptions- Per-instance startup overrides.startOptions.envVars- Per-instance environment variables.startOptions.entrypoint- Entrypoint override for this start only.startOptions.enableInternet- Whether outbound internet access is allowed for this start.waitOptions.portToCheck- Port to probe while starting. If omitted, the class usesdefaultPort, the firstrequiredPortsentry, or a fallback port.waitOptions.signal- Abort signal to cancel startup.waitOptions.retries- Maximum number of start attempts before the method throws.waitOptions.waitInterval- Polling interval in milliseconds between retries.
Returns: Promise<void>. Resolves after the start attempt succeeds and onStart() has run.
Use this when the container does not expose ports, such as a batch job or a cron task, or when you want to manage readiness yourself with waitForPort(). If you need to wait for all ports to be ready, use startAndWaitForPorts() instead.
import { getContainer } from "@cloudflare/containers";
export default { async scheduled(_event, env) { const container = getContainer(env.JOB_CONTAINER, "nightly-report");
await container.start({ entrypoint: ["node", "scripts/nightly-report.js"], envVars: { REPORT_DATE: new Date().toISOString(), }, enableInternet: false, }); },};import { getContainer } from "@cloudflare/containers";
export default { async scheduled(_event, env) { const container = getContainer(env.JOB_CONTAINER, "nightly-report");
await container.start({ entrypoint: ["node", "scripts/nightly-report.js"], envVars: { REPORT_DATE: new Date().toISOString(), }, enableInternet: false, }); },
};Refer to the cron example for a full example.
Poll a single port until it accepts connections.
waitForPort(waitOptions: WaitOptions): Promise<number>Parameters:
waitOptions.portToCheck- Port number to check.waitOptions.signal- Abort signal to cancel waiting.waitOptions.retries- Maximum number of retries before the method throws.waitOptions.waitInterval- Polling interval in milliseconds.
Returns: Promise<number>. The numeric return value is mainly useful when you are coordinating custom readiness logic across multiple waits.
Throws if the port does not become available within the retry limit. Use this after start() when you need to check multiple ports independently or in a specific sequence.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { async warmInspector() { await this.start();
const retryCount = await this.waitForPort({ portToCheck: 9222, retries: 20, waitInterval: 500, });
console.log("Inspector port became ready:", retryCount); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { async warmInspector() { await this.start();
const retryCount = await this.waitForPort({ portToCheck: 9222, retries: 20, waitInterval: 500, });
console.log("Inspector port became ready:", retryCount); }
}Send a signal to the container process.
stop(signal?: 'SIGTERM' | 'SIGINT' | 'SIGKILL' | number): Promise<void>Parameters:
signal- Signal to send. Defaults to'SIGTERM'.
Returns: Promise<void>. Resolves after the signal is sent and pending stop handling has completed.
Defaults to SIGTERM, which gives the process a chance to shut down gracefully. Triggers onStop().
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async fetch(request) { if (new URL(request.url).pathname === "/admin/stop") { await this.stop(); return new Response("Container is stopping"); }
return this.containerFetch(request); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
override async fetch(request: Request): Promise<Response> { if (new URL(request.url).pathname === "/admin/stop") { await this.stop(); return new Response("Container is stopping"); }
return this.containerFetch(request); }
}Immediately kill the container process.
destroy(): Promise<void>Returns: Promise<void>. Resolves after the runtime has destroyed the container.
This sends SIGKILL. Use it when you need the container gone immediately and cannot wait for a graceful shutdown. Triggers onStop().
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async fetch(request) { if (new URL(request.url).pathname === "/admin/destroy") { await this.destroy(); return new Response("Container destroyed"); }
return this.containerFetch(request); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
override async fetch(request: Request): Promise<Response> { if (new URL(request.url).pathname === "/admin/destroy") { await this.destroy(); return new Response("Container destroyed"); }
return this.containerFetch(request); }
}Read the current container state.
getState(): Promise<State>Returns: Promise<State> with:
status- One of'running','healthy','stopping','stopped', or'stopped_with_code'.lastChange- Unix timestamp in milliseconds for the last state change.exitCode- Optional exit code whenstatusis'stopped_with_code'.
running means the container is starting and has not yet passed its health check. healthy means it is up and accepting requests.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { async logState() { const state = await this.getState();
if (state.status === "stopped_with_code") { console.error("Container exited with code", state.exitCode); return; }
console.log("Container status:", state.status); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { async logState() { const state = await this.getState();
if (state.status === "stopped_with_code") { console.error("Container exited with code", state.exitCode); return; }
console.log("Container status:", state.status); }
}Reset the sleepAfter timer.
renewActivityTimeout(): voidReturns: void.
Incoming requests reset the timer automatically. Call this manually from background work, such as a scheduled task or a long-running operation, that should count as activity and prevent the container from sleeping.
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async processJobs(jobIds) { for (const jobId of jobIds) { this.renewActivityTimeout();
await this.containerFetch(`http://localhost/jobs/${jobId}`, { method: "POST", }); } }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async processJobs(jobIds: string[]) { for (const jobId of jobIds) { this.renewActivityTimeout();
await this.containerFetch(`http://localhost/jobs/${jobId}`, { method: "POST", }); } }
}Schedule a method on the class to run later.
schedule<T>(when: Date | number, callback: string, payload?: T): Promise<Schedule<T>>Parameters:
when- Either aDatefor a specific time or a number of seconds to delay.callback- Name of the class method to call.payload- Optional data passed to the callback method.
Returns: Promise<Schedule<T>> with:
taskId- Unique schedule ID.callback- Method name that will be called.payload- Payload that will be passed to the callback.type-'scheduled'for an absolute time or'delayed'for a relative delay.time- Unix timestamp in seconds when the task will run.delayInSeconds- Delay in seconds whentypeis'delayed'.
Do not override alarm() ↗ directly. The Container class uses the alarm handler to manage the container lifecycle, so use schedule() instead.
The following example schedules a recurring health report starting at container startup:
import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
async onStart() { await this.schedule(60, "healthReport"); }
async healthReport() { const state = await this.getState(); console.log("Container status:", state.status); await this.schedule(60, "healthReport"); }}import { Container } from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080;
override async onStart() { await this.schedule(60, "healthReport"); }
async healthReport() { const state = await this.getState(); console.log("Container status:", state.status); await this.schedule(60, "healthReport"); }
}Outbound interception lets you intercept, mock, or block HTTP requests that the container makes to external hosts. This is useful for sandboxing, testing, or proxying outbound traffic through Worker code.
import { Container, ContainerProxy, getContainer,} from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080; enableInternet = true;
static outboundByHost = { "blocked.example.com": () => { return new Response("Blocked", { status: 403 }); }, };
static outbound = async (request, _env, ctx) => { console.log(`[${ctx.containerId}] outbound:`, request.url); return fetch(request); };}
export { ContainerProxy };
export default { async fetch(request, env) { return getContainer(env.MY_CONTAINER).fetch(request); },};import { Container, ContainerProxy, getContainer,} from "@cloudflare/containers";
export class MyContainer extends Container { defaultPort = 8080; enableInternet = true;
static outboundByHost = { "blocked.example.com": () => { return new Response("Blocked", { status: 403 }); }, };
static outbound = async (request, _env, ctx) => { console.log(`[${ctx.containerId}] outbound:`, request.url); return fetch(request); };
}
export { ContainerProxy };
export default { async fetch(request: Request, env) { return getContainer(env.MY_CONTAINER).fetch(request); },};For more information, refer to Handle outbound traffic.
These functions are exported alongside the Container class from @cloudflare/containers.
Get a stub for a named container instance.
getContainer<T>(binding: DurableObjectNamespace<T>, name?: string): DurableObjectStub<T>Parameters:
binding- Durable Object namespace binding for your container class.name- Stable instance name. Defaults tocf-singleton-container.
Returns: DurableObjectStub<T> for the named container instance.
Use this when you want one container per logical entity, such as a user session, a document, or a game room, identified by a stable name.
import { getContainer } from "@cloudflare/containers";
export default { async fetch(request, env) { const { sessionId } = await request.json(); return getContainer(env.MY_CONTAINER, sessionId).fetch(request); },};import { getContainer } from "@cloudflare/containers";
export default { async fetch(request: Request, env) { const { sessionId } = await request.json(); return getContainer(env.MY_CONTAINER, sessionId).fetch(request); },};Get a stub for a randomly selected container instance.
getRandom<T>(binding: DurableObjectNamespace<T>, instances?: number): Promise<DurableObjectStub<T>>Parameters:
binding- Durable Object namespace binding for your container class.instances- Total number of instances to choose from. Defaults to3.
Returns: Promise<DurableObjectStub<T>> for the randomly selected instance.
Use this for stateless workloads where any container can handle any request and you want to spread load across multiple instances.
import { getRandom } from "@cloudflare/containers";
export default { async fetch(request, env) { const container = await getRandom(env.WORKER_POOL, 5); return container.fetch(request); },};import { getRandom } from "@cloudflare/containers";
export default { async fetch(request: Request, env) { const container = await getRandom(env.WORKER_POOL, 5); return container.fetch(request); },};Refer to the stateless instances example for a full example.
Target a different container port while still using fetch().
switchPort(request: Request, port: number): RequestParameters:
request- Request to copy.port- Port to encode into the request headers.
Returns: Request copy with the target port set.
Use this when you need to target a specific port and also need WebSocket support. If you do not need WebSockets, pass the port directly to containerFetch() instead.
import { getContainer, switchPort } from "@cloudflare/containers";
export default { async fetch(request, env) { const container = getContainer(env.MY_CONTAINER); return container.fetch(switchPort(request, 9090)); },};import { getContainer, switchPort } from "@cloudflare/containers";
export default { async fetch(request: Request, env) { const container = getContainer(env.MY_CONTAINER); return container.fetch(switchPort(request, 9090)); },};