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

> Explore the container backend lifecycle in Cloudflare Computer. Learn about creation, monitoring, graceful destruction, and exit handling, all managed via generation tracking for robust performance.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-08-07

---

**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**:

```ts
// 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)](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:

```ts
// 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`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/container-lifecycle.ts#L22-L42):

```ts
// 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`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/container-lifecycle.ts#L50-L81) function implements the core telemetry logic:

```ts
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)`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/container-lifecycle.ts#L64-L66):

```ts
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`](https://github.com/cloudflare/computer/blob/main/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

```ts
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

```ts
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.