> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibely.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming

> How agent output reaches the client — SSE, WebSocket, and queue mode

The agent emits **events**, not request/response payloads. Two transports speak the same event shape, and both are first-class.

<Frame>
  <img src="https://cdn.vibely.sh/doc/v1/engine-streaming-1.webp" alt="Streaming" width="1200" height="675" />
</Frame>

## Transports

### SSE — Server-Sent Events

| Endpoint | Shape |
| - | - |
| `POST /api/v1/vibe/stream` | V1 — generic chunk envelope, easiest to consume |
| `POST /api/v1/vibe/stream_v2` | V2 — named events, recommended for new clients |

Both keep the connection alive with a `: heartbeat\n\n` SSE comment every **15 s**, which beats the typical 30 s idle timeout on Cloudflare / nginx / ELB. Without it, intermediaries silently drop SSE during long agent turns.

V1 wraps every chunk as `data: {"type":…,"data":…}` and terminates with `data: [DONE]`. V2 puts the same `type` on the SSE `event:` line and terminates with `event: done`.

### WebSocket

`WS /api/v1/ws` — bidirectional, 1 MB max payload. The server pushes the same chunk types as SSE. Client-to-server messages are `subscribe`, `unsubscribe`, `cancel` (stops the run), `submit_answers` (resolves an `ask_user_question`), `console_log_data`, and `ping`.

## Event types

Every event carries a `type` from `StreamChunkType` in `src/types/index.ts` — that union is the list. A representative sample:

| Event | When |
| - | - |
| `text` | Streaming model text — visible chat output. (The WebSocket relay emits this as `text_delta`.) |
| `reasoning` | Streaming reasoning text (only for thinking-capable models). |
| `tool_start` | Model decided to call a tool; execution starts on the server. |
| `tool_progress` | A long-running tool reporting partway. |
| `tool_end` | Tool finished; result fed back to the model. |
| `file_streaming_chunk` | A file write is in progress, chunk-by-chunk. |
| `file_streaming_complete` | A file write finished; persisted to DB. |
| `preview_url` | Dev server is up; iframe can mount. |
| `status` | Free-form status string for the UI ("Installing dependencies…"). |
| `ask_user_question` | Agent is waiting for an answer; blocks until `/vibe/answer`. |
| `ask_user_resolved` | The answer arrived; the agent loop resumes. |
| `complete` | Session finished, no more events. |
| `error` | Unrecoverable error; session ends. |

All events are also fan-out via `EventEmitter` to any other subscribers attached to the same project (Redis pub/sub when queue mode is on — see below).

## Client close ≠ session abort

Closing the SSE connection does **not** stop the agent. This is intentional: a flaky network shouldn't kill a 5-minute build. To explicitly stop a session:

```
POST /api/v1/vibe/abort/:projectId
```

The next agent turn checks the abort signal and exits cleanly.

<Frame>
  <img src="https://cdn.vibely.sh/doc/v1/engine-streaming-2.webp" alt="Streaming" width="1200" height="675" />
</Frame>

## `ask_user` flow

```
agent calls ask_user("which auth provider?")
  └─ server emits ask_user_question event, stores resolve()
  └─ agent loop pauses (the tool call is awaiting the resolve)

frontend POST /api/v1/vibe/answer { projectId, answer }
  └─ server calls resolve(answer)
  └─ tool returns the answer to the agent
  └─ agent loop resumes
```

Timeout is **5 minutes**. After that the tool rejects and the agent self-heals (usually by making a reasonable default choice).

## Queue mode (Redis-backed)

When `QUEUE_ENABLED=true`:

```
client POST /vibe/stream
  └─ request enqueued to Redis stream
  └─ HTTP response opens an SSE that subscribes to a Redis pub/sub channel for this projectId

worker picks up the queued job
  └─ runs handleVibeStream(...)
  └─ agent events published to the same channel
  └─ web server relays them to the SSE response
```

Two effects:

1. **Horizontal scaling.** Many web pods can accept requests; a smaller worker pool processes them. A pod restart doesn't kill in-flight builds.
2. **Idempotency.** Re-connecting a client to an in-flight session simply re-subscribes to the channel — no duplicate work.

If queue mode events aren't reaching the client, look at the Redis pub/sub channel name in `src/queue/` and the relay generator in `src/routes/vibe.ts`.

## Backpressure

Neither SSE writer buffers without limit. Before enqueuing a chunk the stream waits while `controller.desiredSize <= 0`, backing off 2 ms → 50 ms until the consumer drains. A slow client (a paused tab, a stalled proxy) therefore slows the generator rather than growing a queue until the process OOMs, and browsers absorb short stalls in their own buffer without the user noticing.


## Related topics

- [Add AI features to your app](/features/backend/ai.md)
- [Build Stream](/api-reference/vibe-stream.md)
- [API Overview](/api-reference/overview.md)
- [Agent Loop](/reference/engine/agent-loop.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.