# Protocol and transport

> JSON-RPC 2.0 wire rules, stdio JSONL vs unix-socket vs websocket listeners, health probes, backpressure code `-32001`, and bounded queue behavior.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/transport.rs`
- `src/main.rs`
- `src/error_code.rs`
- `src/outgoing_message.rs`
- `src/connection_rpc_gate.rs`

---

---
title: "Protocol and transport"
description: "JSON-RPC 2.0 wire rules, stdio JSONL vs unix-socket vs websocket listeners, health probes, backpressure code `-32001`, and bounded queue behavior."
---

`codex app-server` speaks JSON-RPC–shaped messages over a selectable local transport (`--listen`). The default is newline-delimited JSON on stdio; optional listeners include a unix control socket (WebSocket upgrade over UDS) and an experimental TCP WebSocket bind. Ingress and egress use bounded Tokio channels; when the shared ingress queue is full, new client **requests** fail fast with `-32001` instead of blocking the reader.

## JSON-RPC on the wire

Messages follow JSON-RPC 2.0 semantics, but the `"jsonrpc":"2.0"` field is **omitted** on both send and receive. Each line or WebSocket text frame is one JSON object. The wire uses an untagged union of four shapes:

| Shape | Required fields | Notes |
| --- | --- | --- |
| Request | `id`, `method` | Optional `params`, optional W3C `trace` |
| Response | `id`, `result` | `result` is arbitrary JSON |
| Notification | `method` | Optional `params`; no `id` |
| Error | `id`, `error` | `error` has `code`, `message`, optional `data` |

<ParamField body="id" type="string | number" required>
Request/response correlation id. Server-initiated requests use integer ids allocated by the server.
</ParamField>

<ParamField body="method" type="string" required>
v2 RPCs use `<resource>/<method>` (for example `thread/start`, `turn/completed` as a notification).
</ParamField>

<RequestExample>

```json
{"id":0,"method":"initialize","params":{"clientInfo":{"name":"my_client","title":"My Client","version":"0.1.0"}}}
```

</RequestExample>

<Note>
After `initialize` returns, send an `initialized` notification before other RPCs on that connection. Calls made earlier receive `"Not initialized"` (`-32600`); repeat `initialize` receives `"Already initialized"`.
</Note>

Server output mirrors the same shapes: RPC responses and errors, method-named notifications (`turn/started`, `item/agentMessage/delta`, …), and server **requests** (approvals, elicitations) that expect a client response.

## `--listen` transports

<ParamField body="--listen" type="URL" required>
Parsed by `AppServerTransport::from_listen_url`. Default: `stdio://`.
</ParamField>

| URL | Behavior | Framing |
| --- | --- | --- |
| `stdio://` (default) | Single stdin/stdout connection; process exits when stdio closes | One JSON object per line (JSONL); trailing `\n` on write |
| `unix://` | Default path: `$CODEX_HOME/app-server-control/app-server-control.sock` | Raw byte stream → HTTP Upgrade → WebSocket frames |
| `unix://PATH` | Custom socket path; startup lock under `$CODEX_HOME/app-server-control/` | Same as `unix://` |
| `ws://IP:PORT` | TCP listener + HTTP routes (experimental) | One JSON-RPC object per WebSocket **text** frame |
| `off` | No local listener | Use with remote control or in-process embedding |

<Warning>
WebSocket transport (`ws://`) is experimental and unsupported for production. Non-loopback binds require `--ws-auth` (`capability-token` or `signed-bearer-token`).
</Warning>

### Stdio JSONL

The stdio reader uses `BufReader::lines()`: each non-empty line is deserialized as `JSONRPCMessage`. The writer serializes each outgoing message to JSON, appends `\n`, and writes to stdout. This is the default integration path for editor extensions and subprocess clients.

### Unix control socket

`unix://` accepts connections on a private socket (mode `0600` on Unix). Each accepted stream is upgraded with `tokio_tungstenite::accept_async` and then handled like a WebSocket connection (text frames, optional disconnect token). `codex app-server proxy` bridges one raw socket to stdin/stdout for tools that only speak JSONL.

### WebSocket listener

The `ws://` acceptor serves:

- `GET /readyz` → `200 OK` once the listener is accepting connections
- `GET /healthz` → `200 OK` when the request has **no** `Origin` header
- Any request **with** an `Origin` header → `403 Forbidden`
- All other HTTP requests → WebSocket upgrade (subject to auth policy)

Inbound **binary** WebSocket frames are logged and dropped. Ping frames are answered with pong when the control queue has capacity.

## Runtime data path

```mermaid
flowchart TB
  subgraph clients [Clients]
    STDIO[stdio JSONL]
    UDS[unix socket + WS upgrade]
    WS[ws:// listener]
  end

  subgraph transport [Transport tasks]
    READ[Read / parse JSON]
    WRITE[Per-connection writer queue]
  end

  subgraph core [app-server core]
    EVT["transport_event_rx (bounded)"]
    PROC[MessageProcessor + ConnectionRpcGate]
    OUT["outgoing_rx router (bounded)"]
  end

  STDIO --> READ
  UDS --> READ
  WS --> READ
  READ -->|try_send IncomingMessage| EVT
  EVT --> PROC
  PROC --> OUT
  OUT --> WRITE
  WRITE --> STDIO
  WRITE --> UDS
  WRITE --> WS
```

On startup, `run_main_with_transport_options` creates three bounded channels sized to **`CHANNEL_CAPACITY` (128)**:

1. `transport_event_tx` — ingress from all connections  
2. `outgoing_tx` — envelopes to the outbound router  
3. `outbound_control_tx` — per-connection registration and disconnect-all  

Each connection also gets its own `mpsc` writer queue (128 for stdio/unix-socket paths; **32 768** for WebSocket outbound, so burst turn traffic can buffer without tripping ingress overload as quickly).

## Backpressure and `-32001`

When the ingress queue (`transport_event_tx`) is full:

| Incoming message | Server behavior |
| --- | --- |
| **Request** | Respond immediately on the connection with JSON-RPC error code **`-32001`**, message **`Server overloaded; retry later.`** (no `data`). Does not block the transport reader. |
| Notification, response, or client error | **Await** space on the ingress queue (`send().await`) — backpressure propagates to the reader instead of dropping |

If the per-connection outbound queue is also full when sending the overload error, the error response may be **dropped** (logged); the reader still continues.

<ResponseExample>

```json
{"id":7,"error":{"code":-32001,"message":"Server overloaded; retry later."}}
```

</ResponseExample>

<Tip>
Treat `-32001` as retryable: exponential backoff with jitter. Only **requests** get this fast-fail path; high-volume notifications use blocking ingress instead.
</Tip>

### Slow WebSocket clients

Connections opened with a `disconnect_sender` (WebSocket and unix-upgraded paths) use `try_send` on the per-connection outbound queue. If the queue is full, the server **disconnects** that connection rather than blocking broadcasts to other clients. Stdio has no disconnect token; its writer uses blocking `send().await`.

Initialized RPC handlers run under a per-connection **`ConnectionRpcGate`**: closing the gate stops new handlers after shutdown while in-flight work finishes (`TaskTracker`).

## Standard JSON-RPC error codes

Defined in `error_code.rs` and used across handlers:

| Code | Constant | Typical use |
| --- | --- | --- |
| `-32600` | invalid request | Uninitialized connection, bad handshake, duplicate request id |
| `-32601` | method not found | Unknown `method` |
| `-32602` | invalid params | Schema/validation failures; `turn/start` input size (see below) |
| `-32603` | internal error | Unexpected server failures |
| `-32001` | overloaded | Ingress queue saturated (see above) |

Application-level **`input_too_large`** is not a JSON-RPC code. It appears in `error.data` on `-32602` responses from `turn/start` and `turn/steer` when user text exceeds the protocol limit:

<ResponseExample>

```json
{
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Input exceeds the maximum length of … characters.",
    "data": {
      "input_error_code": "input_too_large",
      "max_chars": …,
      "actual_chars": …
    }
  }
}
```

</ResponseExample>

## Logging and verification

| Variable | Effect |
| --- | --- |
| `RUST_LOG` | Tracing filter/verbosity |
| `LOG_FORMAT=json` | One JSON log event per line on stderr |

**Stdio health:** process alive and reading lines; first `initialize` may be scraped for `clientInfo.name`.

**WebSocket health:** after `ws://HOST:PORT` bind:

```bash
curl -s -o /dev/null -w "%{http_code}" "http://HOST:PORT/readyz"
curl -s -o /dev/null -w "%{http_code}" "http://HOST:PORT/healthz"
```

Expect `200` when the listener is up. Requests that include `Origin` should receive `403` on both probe paths.

**Overload:** under synthetic load, concurrent client requests should occasionally receive `-32001` while the process stays responsive (no unbounded memory growth from ingress).

## Related pages

<CardGroup>
  <Card title="Installation" href="/installation">
    Launch `codex-app-server`, default URLs, and environment variables.
  </Card>
  <Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
    Newline framing, request ids, and server requests mid-turn.
  </Card>
  <Card title="Transports and proxy" href="/transports-and-proxy">
    Choosing listeners, control socket path, and `codex app-server proxy`.
  </Card>
  <Card title="Connection lifecycle" href="/connection-lifecycle">
    `initialize` / `initialized`, notification opt-out, and subscribe model.
  </Card>
  <Card title="CLI flags and error codes" href="/cli-flags-and-errors">
    Full flag list, auth, strict config, and troubleshooting overload.
  </Card>
</CardGroup>
