Lifecycle of a Container Backend in Cloudflare Computer: Events, Monitoring, and Exit Handling

The container backend lifecycle consists of four phases: creation with generation tracking, monitor installation for exit detection, graceful destruction using destroyContainerExpectingExit, and structured exit recording via recordExit, with each phase managed through a generation counter to prevent race conditions.

The container backend in Cloudflare Computer provides FUSE-backed virtual filesystem capabilities and hosts the computerd process that powers remote shell and file synchronization features. Unlike the Durable Object (DO) layer—which maintains durable SQLite storage—the container backend is ephemeral and process-bound. Its lifecycle requires careful orchestration to ensure clean exits, proper logging, and state consistency when the DO restarts or the container is recycled.

Container Creation and Monitor Installation

When a Durable Object needs a container, it invokes ctx.container.start(), which returns a handle representing the running computerd instance. Immediately after successful startup, the system must arm the lifecycle monitor:

// From container-lifecycle.ts (lines 91-107)
installContainerMonitor(ctx, container);

This function, defined in [packages/computer/src/backends/container/container-lifecycle.ts](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/container-lifecycle.ts), performs three critical operations:

  1. Increments the generation counter stored in a WeakMap keyed by DurableObjectState
  2. Clears stale exit information from any previous container generation
  3. Attaches a promise handler to container.monitor() that will trigger when the process exits

The monitor itself is a platform-provided promise that resolves when the underlying container process terminates—regardless of whether the exit is graceful (SIGTERM) or abnormal (OOM kill, crash).

Generation Tracking and Race Prevention

The lifecycle system uses a generation counter pattern to prevent stale exit handlers from corrupting state. The implementation stores per-DO state in LIFECYCLE, a module-level WeakMap<DurableObjectState, ContainerLifecycle>:

Field Type Purpose
currentGeneration number Monotonically increases with each installContainerMonitor call
expectedExitGeneration number | null Set when destroyContainerExpectingExit is called, marking intentional shutdown
exit ContainerExitInfo | null Most recent exit timestamp and reason
currentMonitorSettled Promise<void> Awaits completion of the monitor's exit handler

When recordExit processes a container termination, it validates:

// From container-lifecycle.ts (lines 50-81)
if (state.currentGeneration !== generation) {
  // Stale monitor from previous generation—ignore silently
  return;
}

This guard ensures that if a container is destroyed and a new one started before the old monitor resolves, the obsolete handler cannot overwrite the new generation's state.

Graceful Teardown with Expected Exits

Controlled container shutdown uses destroyContainerExpectingExit:

// Example: Graceful container restart
import { destroyContainerExpectingExit, installContainerMonitor } from './container-lifecycle';

async function restartContainer(ctx: DurableObjectState) {
  const container = await ctx.container.get();
  
  // Mark this generation as expecting exit before calling destroy
  await destroyContainerExpectingExit(ctx, container);
  
  // Start fresh container with new generation
  const newContainer = await ctx.container.start({ /* config */ });
  installContainerMonitor(ctx, newContainer);
}

The destroyContainerExpectingExit function:

  1. Records expectedExitGeneration = currentGeneration in the lifecycle state
  2. Invokes container.destroy() to signal platform termination
  3. Awaits currentMonitorSettled to ensure exit logging completes before returning

This sequencing guarantees that the "expected" flag is set before the monitor resolves, ensuring recordExit classifies the termination correctly.

Container Exit Recording and Classification

The recordExit function implements the core telemetry logic:

function recordExit(
  ctx: DurableObjectState,
  generation: number,
  exit: ContainerExitInfo
): void {
  const state = LIFECYCLE.get(ctx);
  if (!state || state.currentGeneration !== generation) return;

  const expected = state.expectedExitGeneration === generation;
  
  // Logging differs by expected vs unexpected
  console[expected ? 'info' : 'warn']('workspace.container.exited', {
    generation,
    expected,
    exitedAt: exit.exitedAt,
    reason: exit.reason,
  });

  state.exit = exit;
  state.currentMonitorSettledResolve?.();
}

Exit classification rules:

  • Expected exit: destroyContainerExpectingExit was called for this generation → logged at info level
  • Unexpected exit: Container died without prior destroyContainerExpectingExit → logged at warn level

Applications can query exit information via containerExitInfo(ctx):

import { containerExitInfo } from './container-lifecycle';

function diagnoseContainer(ctx: DurableObjectState) {
  const info = containerExitInfo(ctx);
  if (!info) return 'No exit recorded—container may still be running';
  
  const elapsed = Date.now() - info.exitedAt;
  return `Exited ${elapsed}ms ago: ${info.reason}`;
}

Container-Lifecycle Events and DO Interactions

The container backend operates alongside three other architectural layers, as documented in docs/11_lifecycle.md#container-lifecycle:

Event Sequence Container State DO State Survival Characteristics
DO restart, container persists Running, FUSE mounted, computerd active New #handle instantiation, fresh capnweb WebSocket Container VFS survives; DO rebuilds session
Container SIGTERM/restart Destroyed, VFS lost, process terminated Running with durable SQLite storage DO storage survives; container state rebuilt from watermarks
Catastrophic failure (both die) Destroyed Destroyed Only ctx.storage (SQLite) persists

The capnweb WebSocket between DO and container is re-established on DO restart without container interruption. However, if the container restarts, the DO must perform a sync round to reconstruct the container's view from stored watermarks in SQLite.

Practical Monitoring Patterns

Pattern 1: Health-Check with Exit Awareness

import { 
  installContainerMonitor, 
  containerExitInfo,
  destroyContainerExpectingExit 
} from './container-lifecycle';

class WorkspaceDO {
  async ensureHealthyContainer() {
    const existing = await this.ctx.container.get();
    const exitInfo = containerExitInfo(this.ctx.state);
    
    // If we have exit info, previous container died
    if (exitInfo && !existing) {
      console.warn('Recovering from unexpected container exit', exitInfo);
    }
    
    const container = existing ?? await this.ctx.container.start({});
    if (!existing) {
      installContainerMonitor(this.ctx.state, container);
    }
    
    return container;
  }
}

Pattern 2: Rolling Restart with Generation Validation

async function rollingRestart(ctx: DurableObjectState) {
  const oldContainer = await ctx.container.get();
  if (!oldContainer) throw new Error('No container to restart');
  
  // Explicit expected-exit protocol
  await destroyContainerExpectingExit(ctx, oldContainer);
  
  // Verify old generation's monitor settled
  const state = getLifecycleState(ctx); // internal helper
  await state.currentMonitorSettled;
  
  // Safe to start new generation
  const newContainer = await ctx.container.start({ 
    imageTag: 'latest' // or config changes
  });
  installContainerMonitor(ctx, newContainer);
  
  return newContainer;
}

Summary

  • Creation: installContainerMonitor arms exit detection and increments generation counter
  • Monitoring: Promise-based container.monitor() resolves on process termination; recordExit validates generation before logging
  • Graceful teardown: destroyContainerExpectingExit marks expected generation and await settlement
  • Classification: Exits matched to expectedExitGeneration determine log severity (info vs warn)
  • DO coupling: Container survives DO restart; DO survives container restart via sync; only SQLite storage survives both dying

Frequently Asked Questions

How does the generation counter prevent race conditions?

The generation counter ensures that slow-resolving exit handlers from obsolete containers cannot corrupt current state. Each installContainerMonitor call increments currentGeneration; recordExit silently drops exits whose generation parameter mismatches. This pattern handles the case where a container is destroyed and replaced before the old monitor promise resolves.

What distinguishes expected from unexpected container exits?

An expected exit occurs when code explicitly calls destroyContainerExpectingExit before container.destroy(), setting expectedExitGeneration to the current value. All other exits—including platform-initiated SIGTERM, OOM kills, and process crashes—are unexpected. The distinction determines log severity and enables operational dashboards to filter routine maintenance from actionable incidents.

Can I retrieve historical exit information after DO restart?

No—exit information is stored in the LIFECYCLE WeakMap keyed by DurableObjectState, which is recreated on each DO instantiation. The containerExitInfo function only returns data from the current incarnation. For durable exit tracking, applications should capture the workspace.container.exited log events in external storage or subscribe to the DO's logging pipeline.

What happens if destroyContainerExpectingExit is not called before container.destroy()?

The exit will be classified as unexpected and logged at warn level. Functionally, the container still terminates, but observability systems will flag the event as potentially anomalous. Always use destroyContainerExpectingExit for deliberate shutdowns to maintain accurate operational metrics and reduce alert noise.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →