Skip to main content

streaming-protocols

Use when designing or debugging streaming in AI systems — SSE, NDJSON, HTTP Streaming, WebSocket, MCP transport, or A2UI transport. Covers the three-layer mental model, bidirectionality, real-world wire formats (OpenAI, Anthropic, MCP), production pitfalls (proxy buffering, compression, mobile reconnection), and the decision guide for which protocol to use when.

소스 정보

저장소
kumaran-is/claude-code-onboarding
최근 소스 활동
2026년 5월 25일 18:14
감지된 SKILL.md 언어
영어
스타
36
포크
21

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
streaming-protocols
description
Use when designing or debugging streaming in AI systems — SSE, NDJSON, HTTP Streaming, WebSocket, MCP transport, or A2UI transport. Covers the three-layer mental model, bidirectionality, real-world wire formats (OpenAI, Anthropic, MCP), production pitfalls (proxy buffering, compression, mobile reconnection), and the decision guide for which protocol to use when.
allowed-tools
Read, Grep, Glob, Bash, WebFetch
metadata
{"triggers":"SSE, server-sent events, NDJSON, HTTP streaming, WebSocket, streaming protocol, MCP transport, streamable HTTP, bidirectional streaming, traceparent, streaming AI, token streaming","related-skills":"a2ui-angular, mcp-builder, agentic-ai-dev","domain":"infrastructure","role":"reference","scope":"architecture","output-format":"guidance"}
last-reviewed
2026-05-25
# Streaming Protocols for AI Systems ## Iron Law **Pick one option from each of the three layers (Transport / Framing / Message). The "which protocol?" question answers itself.** Most streaming confusion comes from treating options at different layers as alternatives. They compose — they are not competitors. --- ## 1. The Three-Layer Mental Model ``` ┌─────────────────────────────────────────────────────────┐ │ MESSAGE LAYER — what each message means │ │ JSON-RPC · A2UI envelope · OpenAI delta · plain JSON │ ├─────────────────────────────────────────────────────────┤ │ FRAMING LAYER — how messages are delimited │ │ SSE events · NDJSON lines · HTTP chunks · WS frames │ ├─────────────────────────────────────────────────────────┤ │ TRANSPORT LAYER — the connection itself │ │ HTTP · HTTP Streaming · WebSocket · stdio │ └─────────────────────────────────────────────────────────┘ ``` ``` Transport opens the pipe. Framing chops the pipe into messages. Message format gives each message meaning. ``` **The most common real-world stack:** ``` HTTP Streaming (transport) └── SSE (framing) └── JSON-RPC or provider-specific JSON (message) ``` **For tool/agent IPC:** ``` stdio (transport) └── newline-delimited JSON (framing) └── JSON-RPC 2.0 (message) ``` --- ## 2. SSE vs HTTP Streaming — The Core Confusion **SSE is not a competitor to HTTP Streaming. SSE is one *kind* of HTTP Streaming.** ``` HTTP Streaming (the behavior: server sends response gradually) │ ├── SSE ← text/event-stream, blank-line framed ├── NDJSON ← one JSON object per line ├── Chunked HTML ← progressive page rendering ├── Raw binary chunks ← video/audio, file downloads └── Custom formats ← gRPC-Web, protobuf streams ``` Asking "SSE vs HTTP Streaming" is like asking "JSON vs HTTP body" — JSON is *one of the things* you can put in a body. **Why people conflate them:** 1. In browsers, `EventSource` is the only ergonomic way to consume HTTP streams. Everything else requires `fetch()` + `ReadableStream` + manual byte parsing. 2. ChatGPT, Claude, Perplexity all use SSE — so "streaming response" colloquially means "SSE response." 3. MCP's transport naming confused everyone: the old transport was "HTTP+SSE"; the new one is "Streamable HTTP" — these sound like alternatives when they're not. **When to pick which framing:** | Scenario | Best framing | Why | |----------|-------------|-----| | Browser receives live updates | **SSE** | Native `EventSource`, auto-reconnect, simple to parse | | AI agent token streaming to a UI | **SSE** | Browser-friendly, typed event names for tool use | | Server-to-server streaming | NDJSON | No event-name overhead, easy to log/replay | | Batch processing pipelines | NDJSON | Line-addressable, replayable, strong tooling | | stdio IPC (MCP local) | NDJSON | OS-pipe-friendly newline framing | | Binary data (video, files) | Raw chunks | No text encoding overhead | | gRPC internal ML platforms | gRPC frames | Strong types, multiplexing, codegen | --- ## 3. Bidirectionality — Half-Duplex vs Full-Duplex | | HTTP/1.1 Streaming | HTTP/2 Streaming | WebSocket | |--|-------------------|-----------------|-----------| | Direction per stream | Half-duplex | Half-duplex within a stream | **Full-duplex** | | Bidirectional in practice? | Only by opening 2+ requests | Yes, via separate streams | Yes, natively | | Auto-reconnect? | Yes (SSE `Last-Event-ID`) | Per protocol | No — implement yourself | | Works through corporate proxies? | Almost always | Almost always | Often blocked or buggy | | Good for AI token streaming? | **Yes** (standard choice) | Yes | Overkill | | Good for voice / collab editing? | No | Marginal | **Yes** | | Server can initiate? | No (only respond) | Server push (limited) | Yes, anytime | **The crisp distinction:** ``` HTTP Streaming = one-direction-at-a-time per request (walkie-talkie) WebSocket = both-directions-simultaneously on one connection (phone call) ``` **Why ChatGPT's "stop generating" button closes the connection:** once the POST is sent, the client is in receive-only mode. There's no upstream channel to send a "stop" message on. The only option is to drop the connection. **Pick HTTP Streaming (SSE) when:** - Data flow is dominantly server → client - You need to traverse corporate proxies and CDNs reliably - You want browser auto-reconnect and resumability for free - You're streaming LLM tokens, agent progress, A2UI updates **Pick WebSocket when:** - Genuinely simultaneous bidirectional flow (voice, multiplayer, collab editing) - Frequent small messages in both directions - Server initiates messages as often as the client does **Practical reality for AI apps:** 95% of the time, SSE is the right choice. WebSocket earns its weight for voice and collaborative editing — not for token streaming. --- ## 4. MCP Bidirectionality Pattern (How HTTP Fakes Full-Duplex) MCP needs bidirectional communication (clients call tools; servers call back via `sampling/createMessage`, `roots/list`, elicitation). It achieves this over half-duplex HTTP using two simultaneous requests: ``` CLIENT MCP SERVER │ │ │── POST /mcp (tools/call) ──────────►│ │◄── HTTP 200, SSE response ──────────│ ← Stream A: responds to client POST │◄── data: {progress notification} │ │◄── data: {final result} │ │ │ │── GET /mcp (Accept: SSE) ─────────►│ │◄── HTTP 200, SSE response ──────────│ ← Stream B: long-lived GET for │◄── data: {server→client request} │ server-initiated messages │ │ │── POST /mcp (response to above) ───►│ ← Client replies via new POST │◄── HTTP 200 ────────────────────────│ ``` "Bidirectional messaging" in MCP = messages flow in both directions across two streams, **not** both directions on a single TCP stream. **Two headers that carry the operational glue:** | Header | Purpose | |--------|---------| | `Mcp-Session-Id` | Set by server at init. Must be echoed on every subsequent request. Missing = silent state bugs. | | `Last-Event-ID` | Standard SSE header. Client sends last received ID on reconnect; server replays missed events. | **SSE deprecation note:** SSE was deprecated as a *standalone MCP transport* (March 2025, replaced by Streamable HTTP). SSE itself is alive and well for LLM streaming, dashboards, and all browser-facing live updates. --- ## 5. Real-World Wire Formats ### OpenAI / Compatible APIs (SSE, bare JSON) ``` data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"The"},"index":0}]} data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":" answer"},"index":0}]} data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop","index":0}]} data: [DONE] ``` - Bare JSON (not JSON-RPC) — request/response pairing is implicit in the HTTP exchange - `[DONE]` sentinel is OpenAI's convention, not part of the SSE spec - Client extracts `choices[0].delta.content` and appends to rendered message ### Anthropic (SSE, typed events) ``` event: message_start data: {"type":"message_start","message":{"id":"msg_01...","role":"assistant"}} event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The answer"}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: message_stop data: {"type":"message_stop"} ``` - `event:` field carries semantic type — clients dispatch on type rather than inspecting payload shape - Makes tool use, thinking blocks, and multi-modal content cleaner to handle - Mid-stream errors arrive as `event: error` with HTTP status still 200 ### MCP (JSON-RPC over SSE or newline-delimited) ``` event: message data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t1","progress":30}} event: message data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"done"}]}} ``` ### A2UI over SSE ``` event: a2ui data: {"version":"v0.9","createSurface":{"surfaceId":"ticket_1","catalogId":"..."}} event: a2ui data: {"version":"v0.9","updateDataModel":{"surfaceId":"ticket_1","path":"/status","value":"Open"}} ``` ### A2UI over NDJSON (replay / eval / debugging) ```jsonl {"version":"v0.9","createSurface":{"surfaceId":"ticket_1","catalogId":"..."}} {"version":"v0.9","updateDataModel":{"surfaceId":"ticket_1","path":"/status","value":"Open"}} {"version":"v0.9","updateComponents":{"surfaceId":"ticket_1","components":[...]}} ``` --- ## 6. Production Pitfalls Checklist ### ❌ Proxy Buffering (Silent Stream Killer) Nginx, CloudFront, and most CDNs buffer responses by default. Your "streaming" tokens arrive in one batch. **Fix:** ```nginx location /stream { proxy_buffering off; add_header X-Accel-Buffering no; } ``` Also set `X-Accel-Buffering: no` as a response header from your app server. ### ❌ gzip / Brotli Breaks SSE Compression middleware accumulates bytes before compressing — turns smooth token flow into chunky bursts. **Fix:** Disable compression for `text/event-stream` responses. The bandwidth saving is not worth the latency cost. ```ts // Express example app.use(compression({ filter: (req, res) => { if (res.getHeader('Content-Type') === 'text/event-stream') return false; return compression.filter(req, res); }, })); ``` ### ❌ SSE Connection Timeouts on Long Agent Runs Long agent runs exceed default proxy timeouts (often 60s). Connection drops mid-stream. **Fix:** Send periodic keepalive comments: ``` : keepalive ``` (A colon-prefixed line is an SSE comment — ignored by clients, but prevents timeout.) ### ❌ Mobile Reconnection Without `Last-Event-ID` iOS kills HTTP connections when the app backgrounds. Without `Last-Event-ID`, the stream restarts from scratch on reconnect. **Fix:** 1. Include `id:` field on every SSE event 2. Buffer events server-side for ≥60 seconds after sending 3. On reconnect, client sends `Last-Event-ID` header; server replays from that point ```ts // Client: use exponential backoff — default EventSource reconnect is too aggressive const es = new EventSource('/stream'); let retryDelay = 1000; es.onerror = () => { setTimeout(() => reconnect(), retryDelay); retryDelay = Math.min(retryDelay * 2, 30000); }; ``` 4. Surface "reconnecting..." state to the user — silent staleness is worse than a visible indicator 5. For long agent runs (>10 min backgrounded): provide a polling fallback that returns current state ### ❌ Missing `traceparent` Propagation A single user action may generate 30+ network calls. Without a correlation ID, cross-service debugging is impossible. **Fix:** Generate `traceparent` (W3C Trace Context) at the edge; forward it on every outbound call. ``` User action └─► Backend generates traceparent: 00-{trace_id}-{span_id}-01 ├─► POST to LLM API — Header: traceparent ├─► POST to MCP server — Header: traceparent, Mcp-Session-Id └─► SSE response to client — echo traceparent back ``` Every modern observability tool (Langfuse, Datadog, Honeycomb, Grafana Tempo) understands `traceparent`. Include it in NDJSON logs so you can join logs across services after the fact. ### ❌ Conflating JSON-RPC `id` and SSE `id` They are unrelated and live at different layers. | Field | Layer | Purpose | |-------|-------|---------| | JSON-RPC `id` | Message | Correlates request to response within a JSON-RPC session | | SSE `id` | Framing | Enables reconnection via `Last-Event-ID` header | ### ❌ Treating Streamable HTTP and SSE as Competitors Streamable HTTP (MCP) is a *transport pattern*. SSE is *one of the response formats* Streamable HTTP can return. They compose. ### ❌ WebSocket "Because It's Realtime" If data flow is server→client only (which most LLM streaming is), WebSocket adds operational complexity without benefit. SSE is simpler, proxy-friendly, and auto-reconnecting. Reach for WebSocket only when you genuinely need bidirectional realtime. --- ## 7. Decision Guide | Your need | Transport | Framing | Message | |-----------|-----------|---------|---------| | Chat UI with token streaming | HTTP Streaming | SSE | Provider JSON (OpenAI/Anthropic) | | Local AI app calling tools | stdio | newline-delimited | JSON-RPC (MCP) | | Remote AI app calling tools | HTTP Streaming | JSON or SSE | JSON-RPC (MCP) | | Agent pushing UI updates (A2UI) | HTTP Streaming | SSE | A2UI envelope | | Recording agent runs (eval/replay) | File / HTTP POST | NDJSON | App-specific trace | | Bulk LLM inference | HTTP | NDJSON | Provider JSON (Batch API) | | Voice / collaborative editing | WebSocket | WS frames | App-specific | | High-perf server-to-server AI | HTTP/2 | gRPC frames | Protobuf | | Simple one-shot answer | HTTP | — | Plain JSON |
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기