# Streaming responses

> Watch Server-Sent Events, JSON-lines streams, multipart GraphQL responses and long chunked bodies event by event in Cloak Proxy, live, with when each event arrived. Streams pass through unbuffered.

Source: https://docs.subcloak.com/cloak-proxy/inspect/streams/

Added in Cloak Proxy 0.4.0.

Some responses arrive a piece at a time: an AI assistant's reply token by token, live scores, order updates, build logs, a GraphQL subscription. Cloak Proxy passes these streams on to the app as they arrive, and the [inspector](https://docs.subcloak.com/cloak-proxy/inspect/inspector/) shows them event by event in a **Messages** tab, updating while the stream is open. Each event shows when it arrived and how long it took after the one before, so a slow first token or a stalled stream is easy to spot.

*[Screenshot: The Messages tab of a Server-Sent Events response: an Ended badge, a filter field, an All types menu and the summary 16 events, first after 381 ms, longest gap 1.418 s. Below it, a strip with a tick for each event, then a table of events with number, time, gap, type and data: message_start at 381 ms, ping, content_block_start, then content_block_delta events. Event 9 is selected, arriving 1.42 s after the previous one, and its JSON data opens in a tree below.. An assistant's reply streamed as SSE. Event 9 arrived after a 1.42 s stall.]*

## What counts as a stream

| Response | Shown as | Example |
|---|---|---|
| `text/event-stream` | **Messages**: one row per Server-Sent Event | Most AI APIs, live feeds, notifications |
| `application/x-ndjson`, `application/jsonl`, `application/json-seq` and similar | **Messages**: one row per JSON line | Local model servers, log and export APIs |
| `multipart/mixed` | **Messages**: one row per part | GraphQL `@defer` and `@stream` (incremental delivery) |
| Any other response with no `Content-Length` that is still arriving after half a second | **Chunks**: one row per network read | Long polling, progress output, chunked feeds |

The traffic list marks these requests with a teal badge before the URL: **SSE**, **JSON lines**, **Multipart** or **Streamed**, and **Live ·** in front while the stream is still open.

## Streams pass through as they arrive

Cloak Proxy forwards a stream's bytes to the app as soon as the server sends them, instead of waiting for the whole response. Event sources, chat UIs and progress bars keep working through the proxy, and closing the stream in the app closes it at the server too.

> **When a stream is buffered:**
> A stream is read whole before the app gets it when a tool needs the complete body: a [Breakpoint](https://docs.subcloak.com/cloak-proxy/tools/breakpoint/) that pauses responses, a [script](https://docs.subcloak.com/cloak-proxy/tools/scripting/) with `onResponse`, or a [Rewrite](https://docs.subcloak.com/cloak-proxy/tools/rewrite/) rule that changes the response body. The app then sees the events all at once, when the stream ends, and the Messages tab shows **Buffered** with no arrival times. Rewrite rules that only change headers or the status code still stream. [Network Conditions](https://docs.subcloak.com/cloak-proxy/tools/network-conditions/) slow a stream down part by part rather than holding it.

## Watch a stream

1. Make sure the connection is decrypted. For HTTPS, turn on [SSL Proxying](https://docs.subcloak.com/cloak-proxy/capture/ssl-proxying/) for the host or the app.
2. Select the request in the traffic list. A stream that's still open shows **Live ·** in its badge.
3. The response pane opens on the **Messages** tab, with the event count. Events are added as they arrive, and the list scrolls to the newest one.
4. Click an event to see its data below the table. Selecting an event stops the list following new ones. Click the follow button at the right of the toolbar to follow again.

Streams remember their own tab: if you switch a stream to **Body** or **Raw**, the next stream you select opens on that tab too, while ordinary responses keep theirs.

*[Screenshot: The Messages tab of an order-updates stream that is still open: a green Live badge, five events with ids 1 to 4, a retry event that sets the reconnect delay to 5000 ms, status and location events with gaps of 6.28 s and 8.50 s in orange, and Waiting for more… under the last row.. A stream that's still open. The two long waits are orange.]*

### Messages table

| Column | Shows |
|---|---|
| **#** | The event's position in the stream |
| **Time** | When the event finished arriving, counted from the start of the request |
| **Gap** | How long after the previous event it arrived. Orange from 1 second, red from 10 seconds. |
| **Type** | The SSE `event:` field (`message` when there is none), or a multipart part's `Content-Type`. Blocks that only set the reconnect delay show as `retry`. Not shown for JSON lines and chunks. |
| **ID** | The SSE `id:` field, when any event has one |
| **Size** | The size of the event's data |
| **Data** | The data on one line. Line breaks inside it show as ⏎. |

Two small badges can follow the data:

| Badge | Meaning |
|---|---|
| **Cut off** | The stream ended in the middle of this event: it had no blank line (SSE), no newline and wasn't valid JSON (JSON lines), or no closing boundary (multipart). |
| **No data** | An SSE block with fields but no `data:` line. Browsers' `EventSource` doesn't deliver these, so the app never sees them. |

### Toolbar

| Control | What it does |
|---|---|
| State badge | **Live** while the response is streaming. **Ended** when the server finished it, **Closed by app** when the app hung up first, **Broke** when the connection to the server dropped (hover to see why), **Buffered** when a tool held it. |
| **Filter events** | Shows events whose data, type or id contains your text, ignoring case |
| **All types** | Shows one event type. Each type lists how many events it has. Only there when the stream has more than one type. |
| Summary | How many events, how long after the request the first one arrived, and the longest gap. **times estimated** appears for compressed streams (see below). |
| Follow button | While the stream is live: keeps the newest event in view |

### Arrival strip

Under the toolbar, a strip runs from the start of the request (**0**) to the last event. The gray bar at its start is the wait for the response headers, and each tick is an event, so bursts and stalls show at a glance. Ticks of events hidden by a filter are fainter. Click anywhere on the strip to select the nearest event.

### Read an event

The lower half shows the selected event's number, type, id, `retry` value, when it arrived, the gap, and its size. Its data opens in the [body viewer](https://docs.subcloak.com/cloak-proxy/inspect/body-viewers/): JSON as a tree you can filter and query with JSONPath or jq, other text as **Raw**, binary chunks in **Hex**. Click the copy button to copy the data.

Right-click an event for **Copy Data**, **Copy Data as Pretty JSON**, **Copy Event as Sent** (with its `event:`, `id:` and `data:` lines), or **Show Only** *type* **Events**.

*[Screenshot: A JSON-lines stream from a local model server: nine events 31 ms apart, each a JSON object with model, created_at and message, and event 4 open in a tree.. JSON lines from a local model server, one event per line.]*

## GraphQL subscriptions

When a stream carries GraphQL, the response pane adds a **GraphQL** tab that groups the traffic by operation. It recognizes:

| Transport | How it's recognized |
|---|---|
| `graphql-transport-ws` (the `graphql-ws` library) | A WebSocket with that subprotocol, or whose messages use `subscribe` and `next` |
| `graphql-ws` (the older subscriptions-transport-ws) | A WebSocket with that subprotocol, or whose messages use `start` and `data` |
| GraphQL over SSE | A GraphQL request answered with `text/event-stream` (`next` and `complete` events) |
| Incremental delivery | A GraphQL request answered with `multipart/mixed` (`@defer`, `@stream`) |

*[Screenshot: The GraphQL tab of a WebSocket: graphql-transport-ws, Acknowledged, 2 operations, 1 active, 6 results. The subscription OnMessage (id 1, Active, 4 results, first result at 4.000 s) is selected above subscription OrderStatus (Completed, 2 results). Below, its Results tab lists four results 7.30 s apart, and the last one opens as a JSON tree with messageAdded text It left the warehouse this morning.. Two subscriptions on one WebSocket, and the results of the selected one.]*

| Column | Shows |
|---|---|
| **Operation** | The kind (`subscription`, `query`, `mutation`) and name, or **Anonymous** |
| **ID** | The id the protocol gave the operation |
| **Status** | **Active** while results may follow, **Completed** when the server finished it, **Failed** when it sent an error, **Stopped** when the app stopped it or the socket closed |
| **Results** | How many results arrived |
| **First result** | When the first result arrived, from the start of the connection |

Select an operation to see its **Results** (each with its time and gap, and its payload as JSON), its **Query**, its **Variables** and any **Errors**. Results that carry `errors` have a red **errors** badge. The tab's header names the transport and, for WebSockets, whether the server answered `connection_init` with `connection_ack`.

The **Messages** tab still lists every frame or event, including protocol messages such as `ping`, `pong` and `connection_init`.

## Timing details

| Detail | How it's measured |
|---|---|
| Event time | When the network read containing the event's last byte reached Cloak Proxy, counted from when the request started |
| Response headers | When the response head arrived (the gray bar on the arrival strip, and `headersMs` in MCP) |
| Gap | The difference between an event's time and the previous event's |
| Compressed streams | Cloak Proxy decompresses gzip, deflate and Brotli streams to read the events, then places each one in time by its position in the stream. The summary says **times estimated**. |
| Very long streams | Up to 20,000 network reads keep their own times. Beyond that, neighbouring reads are merged, so some events take the time of the read after them, never an earlier one. |
| Body size | Events past the [Max captured body size](https://docs.subcloak.com/cloak-proxy/capture/network-settings/#limit-the-captured-body-size) aren't recorded, but the stream keeps flowing to the app. |

Arrival times are saved in Cloak Proxy session files. HAR files have no place for them, so a stream opened from a HAR shows its events without times.

## With an AI assistant

The `get_stream_events` MCP tool returns a streamed response event by event, with each event's type, id, data (parsed when it's JSON), time and gap, plus the time to the first event, the average and longest gap, and how the stream ended. For GraphQL streams it adds each operation with its status, result count and last result. Pass `after` with the `lastIndex` of the previous call to get only newer events while a stream is still open. See [MCP tools reference](https://docs.subcloak.com/cloak-proxy/automation/mcp-tools-reference/#get_stream_events).

> Use Cloak Proxy to look at request #42's stream. How long until the first token, and where did it stall?

## From the command line

`cloakctl events` prints a stream one event per line: index, time, gap, type, id and data.

```bash title="Terminal"
cloakctl events 42                    # from the running app
cloakctl events 42 --follow           # keep printing until the stream ends
cloakctl events 42 --type message_delta
cloakctl events 42 --session run.cloaksession
```

```text title="Output"
# https://api.assistant.example/v1/messages · SSE · 16 events
1     381 ms               message_start  {"type":"message_start",…}
2     382 ms     +1 ms     ping  {"type":"ping"}
9     2.121 s    +1.418 s  content_block_delta  {"type":"content_block_delta",…}
```

| Option | What it does |
|---|---|
| `--follow` | Keeps printing new events every half second until the stream ends |
| `--type TYPE` | Only events of this type |
| `--json` | Prints the `get_stream_events` result instead |
| `--session FILE` / `--har FILE` | Reads the request from a saved session instead of the running app |
| `--port N` | The app's port, when it isn't the configured one |

It exits `0` when the stream ended cleanly or is still live, `2` when the server broke the stream or its last event was cut off, and `1` when the request isn't a stream. In CI, record with `cloakctl run --record run.cloaksession`, then check a streaming endpoint with `cloakctl events ID --session run.cloaksession`.

## Limits

- Streams over WebSocket are shown in the WebSocket [Messages tab](https://docs.subcloak.com/cloak-proxy/inspect/websocket-and-graphql/#websocket-messages); only GraphQL ones also get the GraphQL tab.
- Editing a stream's events in flight (dropping, delaying or injecting events) isn't available yet. A [Breakpoint](https://docs.subcloak.com/cloak-proxy/tools/breakpoint/) on the response holds the whole stream and lets you edit it as one body.
- gRPC streaming calls are shown in the body viewer, not as events.

## Related

  - [WebSocket and GraphQL](https://docs.subcloak.com/cloak-proxy/inspect/websocket-and-graphql/): Frames on a WebSocket, and GraphQL over HTTP.
  - [Body viewers](https://docs.subcloak.com/cloak-proxy/inspect/body-viewers/): The JSON tree, JSONPath and jq used for each event.
  - [Inspector](https://docs.subcloak.com/cloak-proxy/inspect/inspector/): Every request and response tab.
  - [Network Conditions](https://docs.subcloak.com/cloak-proxy/tools/network-conditions/): Slow a stream down to test a spinner or a timeout.
