Skip to content

Status Hooks

Execute Workers code in reaction to Container status changes

Last updated View as MarkdownAgent setup

Use monitor() with the Durable Object Container API to run code after the Container exits or errors. The Container class adds named lifecycle hooks and an inactivity callback.

src/index.jsjs
import { DurableObject } from "cloudflare:workers";

export class MyContainer extends DurableObject {
	ready;

	constructor(ctx, env) {
		super(ctx, env);
		ctx.blockConcurrencyWhile(() =>
			ctx.container.setInactivityTimeout(5 * 60 * 1000),
		);
	}

	async fetch(request) {
		const container = this.ctx.container;
		if (!container.running) {
			this.ready = undefined;
		}
		this.ready ??= this.startAndMonitor().catch((error) => {
			this.ready = undefined;
			throw error;
		});
		await this.ready;

		return container.getTcpPort(4000).fetch(request);
	}

	async startAndMonitor() {
		const container = this.ctx.container;
		if (!container.running) {
			container.start();
		}

		this.ctx.waitUntil(
			container
				.monitor()
				.then(() => console.log("Container stopped"))
				.catch((error) => console.error("Container error:", error)),
		);

		const port = container.getTcpPort(4000);
		let lastError;
		for (let attempt = 0; attempt < 50; attempt++) {
			try {
				await port.fetch("http://container/");
				console.log("Container successfully started");
				return;
			} catch (error) {
				lastError = error;
				await scheduler.wait(100);
			}
		}
		throw lastError;
	}
}
src/index.tsts
import { DurableObject } from "cloudflare:workers";

interface Env {}

export class MyContainer extends DurableObject<Env> {
	private ready: Promise<void> | undefined;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		ctx.blockConcurrencyWhile(() =>
			ctx.container!.setInactivityTimeout(5 * 60 * 1000),
		);
	}

	async fetch(request: Request): Promise<Response> {
		const container = this.ctx.container!;
		if (!container.running) {
			this.ready = undefined;
		}
		this.ready ??= this.startAndMonitor().catch((error: unknown) => {
			this.ready = undefined;
			throw error;
		});
		await this.ready;

		return container.getTcpPort(4000).fetch(request);
	}

	private async startAndMonitor(): Promise<void> {
		const container = this.ctx.container!;
		if (!container.running) {
			container.start();
		}

		this.ctx.waitUntil(
			container
				.monitor()
				.then(() => console.log("Container stopped"))
				.catch((error: unknown) => console.error("Container error:", error)),
		);

		const port = container.getTcpPort(4000);
		let lastError: unknown;
		for (let attempt = 0; attempt < 50; attempt++) {
			try {
				await port.fetch("http://container/");
				console.log("Container successfully started");
				return;
			} catch (error) {
				lastError = error;
				await scheduler.wait(100);
			}
		}
		throw lastError;
	}
}
import { Container } from "@cloudflare/containers";

export class MyContainer extends Container {
	defaultPort = 4000;
	sleepAfter = "5m";

	override onStart() {
		console.log("Container successfully started");
	}

	override onStop(stopParams) {
		if (stopParams.exitCode === 0) {
			console.log("Container stopped gracefully");
		} else {
			console.log("Container stopped with exit code:", stopParams.exitCode);
		}

		console.log("Container stop reason:", stopParams.reason);
	}

	override async onActivityExpired() {
		console.log("Container became idle, stopping it now");
		await this.stop();
	}

	override onError(error: string) {
		console.log("Container error:", error);
	}
}

The monitor() promise in the raw API does not include an exit code or stop reason. The setInactivityTimeout() method does not invoke a callback when the timeout expires. Use the Container class lifecycle hooks when you need those higher-level events.

Was this helpful?