# How Exec Events Are Encoded and Decoded Across the RPC Boundary in Computer

> Learn how Computer encodes exec events as Cap'n Proto binary structs for RPC transport and decodes them into TypeScript objects for streaming and replay from SQLite.

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

---

**Exec events in Computer are encoded as Cap'n Proto binary structs for transport over a WebSocket-based RPC layer, then decoded by the client stub into TypeScript `ExecEvent` objects that can be streamed and replayed from persistent SQLite storage.**

The Computer project by Cloudflare implements a remote execution system where shell commands run in a server-side runtime and stream output back to clients. Understanding the encoding and decoding mechanism for these **exec events across the RPC boundary** is essential for developers building on or debugging this architecture.

## The Exec Event Lifecycle: From Spawn to Stream

The encoding and decoding flow spans six distinct phases, each handled by specific components in the codebase.

### 1. Command Invocation via RPC Interface

The client initiates execution by calling the `exec()` method. The RPC contract is defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts), where the server-side `Runner.exec` receives an `ExecInput` struct containing the command source and configuration.

### 2. Event Generation in the Runner

Inside [`packages/computerd/src/exec/runner.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/exec/runner.ts), the `Runner.exec` method spawns the process. Each output fragment—stdout, stderr, exit status, or error—is wrapped in an `ExecEvent` record and persisted via the `enqueue` function to the SQLite `computerd_exec_log` table.

### 3. Wire Encoding with Cap'n Proto

The **encoding mechanism** is **Cap'n Proto binary serialization**. The `ExecEvent` struct (defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts)) is serialized to its wire format and transmitted over the persistent WebSocket underlying the Cap'n Web transport. The wire contract is documented in [`docs/08_capnweb_interface.md`](https://github.com/cloudflare/computer/blob/main/docs/08_capnweb_interface.md).

### 4. Stream Construction for Replay

The `replayToStream` function in [`packages/computerd/src/exec/runner.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/exec/runner.ts) constructs a `ReadableStream<ExecEvent>`. It queries the SQLite log, converts stored rows back to Cap'n Proto structs, and pushes them into the stream—enabling both live and historical event consumption.

### 5. Decoding on the Client Side

The **decoding mechanism** runs in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts). The generated RPC client stub receives binary Cap'n Proto messages, deserializes them into TypeScript `ExecEvent` objects, and yields them from the `ReadableStream` returned by the original `exec()` call.

### 6. Event Consumption in User Code

The caller iterates the decoded stream using `for await...of` syntax to process stdout chunks, stderr output, and the final exit code.

## Encoding Format: Cap'n Proto Struct Details

Cap'n Proto provides several advantages for this use case:

- **Zero-copy deserialization** — The client can access fields without parsing overhead
- **Packed encoding** — `ExecEvent` structs are compact on the wire
- **Schema evolution** — New event types can be added without breaking existing clients
- **Streaming support** — Messages can be sent incrementally over the WebSocket

The `ExecEvent` type discriminates on a `type` field with variants including `"stdout"`, `"stderr"`, `"exit"`, and `"error"`. Each variant carries appropriate payload fields (e.g., `data: Uint8Array` for output chunks, `code: number` for exit status).

## Code Examples: Encoding and Decoding in Practice

### Running a Command and Streaming Decoded Events

```typescript
import { createWorkspaceClient } from "@cloudflare/computer";

const client = await createWorkspaceClient(/* workspace config */);

// Encoding happens server-side; client receives decoded ExecEvent stream
const handle = await client.exec({
  source: "echo hello && echo world >&2 && exit 3"
});

for await (const ev of handle.events) {
  switch (ev.type) {
    case "stdout":
      process.stdout.write(new TextDecoder().decode(ev.data));
      break;
    case "stderr":
      process.stderr.write(new TextDecoder().decode(ev.data));
      break;
    case "exit":
      console.log(`\n[exit: ${ev.code}]`);
      break;
    case "error":
      console.error(`\n[error: ${ev.message}]`);
      break;
  }
}

```

### Replaying Events from Persistent Storage

The **decoding mechanism** supports replay for clients that disconnect and reconnect:

```typescript
// Re-attach to completed exec with full history
const replay = await client.getExec({
  id: handle.id,
  after: 0  // Sequence number offset
});

for await (const ev of replay.events) {
  // Same ExecEvent decoding as live stream
  processEvent(ev);
}

```

The `after` parameter enables resumption from any point in the log, with the server re-encoding stored events through the same Cap'n Proto pipeline.

### Controlling and Cleaning Up Executions

```typescript
// Signal the running process
await client.killExec({ id: handle.id, signal: "SIGTERM" });

// Dispose releases SQLite log entries and server-side resources
await client.disposeExec({ id: handle.id });

```

## Key Implementation Files

| File Path | Responsibility |
|-----------|---------------|
| [`packages/computerd/src/exec/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/exec/types.ts) | TypeScript type definitions for `ExecEvent` and related structures |
| [`packages/computerd/src/exec/runner.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/exec/runner.ts) | Event generation, SQLite persistence, and `replayToStream` implementation |
| [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts) | RPC method signatures and Cap'n Proto struct declarations |
| [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) | Client stub with Cap'n Proto decoding logic |
| [`docs/08_capnweb_interface.md`](https://github.com/cloudflare/computer/blob/main/docs/08_capnweb_interface.md) | Wire protocol specification for the encoding format |

## Performance and Reliability Considerations

- **Backpressure handling** — The `ReadableStream` interface propagates flow control across the RPC boundary
- **Persistence guarantees** — SQLite logging ensures no event loss even if the client disconnects
- **Binary efficiency** — Cap'n Proto's packed encoding minimizes bandwidth for high-volume output
- **Type safety** — Generated TypeScript bindings from Cap'n Proto schemas prevent decoding errors

## Summary

- **Encoding mechanism**: Cap'n Proto binary serialization of `ExecEvent` structs in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts)
- **Transport**: WebSocket-based Cap'n Web protocol with persistent connections
- **Persistence**: SQLite `computerd_exec_log` table enables replay and resilience
- **Decoding mechanism**: Generated client stub in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) reconstructs TypeScript objects
- **Streaming interface**: `ReadableStream<ExecEvent>` provides idiomatic async iteration for consumers

## Frequently Asked Questions

### What encoding format does Computer use for RPC messages?

Computer uses **Cap'n Proto** for all RPC message encoding, including exec events. This binary format provides zero-copy deserialization, compact wire representation, and strong schema evolution guarantees compared to JSON or Protocol Buffers.

### Can exec events be replayed after a client disconnects?

Yes. The `computerd` runtime persists all `ExecEvent` records to SQLite via the `enqueue` function in [`packages/computerd/src/exec/runner.ts`](https://github.com/cloudflare/computer/blob/main/packages/computerd/src/exec/runner.ts). Clients can call `getExec({ id, after })` to request a decoded stream starting from any sequence number, enabling seamless reconnection and historical replay.

### How does the client decode exec events from the wire?

The generated RPC client stub in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) handles Cap'n Proto decoding automatically. Application code receives fully-typed TypeScript `ExecEvent` objects through a standard `ReadableStream`, with no manual deserialization required.

### What happens to exec events after a process completes?

Events remain in the SQLite `computerd_exec_log` table until `disposeExec({ id })` is called. This allows late-joining clients to retrieve complete output, but requires explicit cleanup to prevent unbounded storage growth.