UI はローカライズ済みです。スキルの紹介と説明文書は選択した言語へ AI が自動翻訳し、コード・コマンド・パス・専門用語は原文のままです。原作者の文書が優先されます。
スキルを探す
デザイン · SKILL.md

streaming-protocols

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 …

Independent GitHub snapshot · Automated classification · Original documentation governs

スキルの説明

GitHub

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:

ScenarioBest framingWhy
Browser receives live updatesSSENative EventSource, auto-reconnect, simple to parse
AI agent token streaming to a UISSEBrowser-friendly, typed event names for tool use
Server-to-server streamingNDJSONNo event-name overhead, easy to log/replay
Batch processing pipelinesNDJSONLine-addressable, replayable, strong tooling
stdio IPC (MCP local)NDJSONOS-pipe-friendly newline framing
Binary data (video, files)Raw chunksNo text encoding overhead
gRPC internal ML platformsgRPC framesStrong types, multiplexing, codegen

3. Bidirectionality — Half-Duplex vs Full-Duplex

HTTP/1.1 StreamingHTTP/2 StreamingWebSocket
Direction per streamHalf-duplexHalf-duplex within a streamFull-duplex
Bidirectional in practice?Only by opening 2+ requestsYes, via separate streamsYes, natively
Auto-reconnect?Yes (SSE Last-Event-ID)Per protocolNo — implement yourself
Works through corporate proxies?Almost alwaysAlmost alwaysOften blocked or buggy
Good for AI token streaming?Yes (standard choice)YesOverkill
Good for voice / collab editing?NoMarginalYes
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:

HeaderPurpose
Mcp-Session-IdSet by server at init. Must be echoed on every subsequent request. Missing = silent state bugs.
Last-Event-IDStandard 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)

{"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:

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.

// 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
// 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);
};
  1. Surface "reconnecting..." state to the user — silent staleness is worse than a visible indicator
  2. 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.

FieldLayerPurpose
JSON-RPC idMessageCorrelates request to response within a JSON-RPC session
SSE idFramingEnables 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 needTransportFramingMessage
Chat UI with token streamingHTTP StreamingSSEProvider JSON (OpenAI/Anthropic)
Local AI app calling toolsstdionewline-delimitedJSON-RPC (MCP)
Remote AI app calling toolsHTTP StreamingJSON or SSEJSON-RPC (MCP)
Agent pushing UI updates (A2UI)HTTP StreamingSSEA2UI envelope
Recording agent runs (eval/replay)File / HTTP POSTNDJSONApp-specific trace
Bulk LLM inferenceHTTPNDJSONProvider JSON (Batch API)
Voice / collaborative editingWebSocketWS framesApp-specific
High-perf server-to-server AIHTTP/2gRPC framesProtobuf
Simple one-shot answerHTTP—Plain JSON

8. NDJSON's Real-World Niche

NDJSON rarely appears in the live request path (SSE wins there). It dominates in three places:

  1. stdio IPC — MCP local servers, language servers, CLI tools. Newline framing is OS-pipe-friendly.
  2. Batch / bulk APIs — OpenAI Batch API, Anthropic Batch API return NDJSON. Common Crawl, eval traces, log shipping (Vector, Fluent Bit) all favor NDJSON.
  3. Replay and eval — Recording an agent run as NDJSON gives a perfect, line-addressable trace. Langfuse, Braintrust, W&B Weave ingest these directly.
SSE        → live, server→client, browser-facing
NDJSON     → batch, logs, replay, stdio, server-to-server
WebSocket  → genuinely bidirectional realtime (voice, collab editing)
Plain JSON → one-shot response, no streaming needed

Cheat Sheet

Three rules that catch 90% of bugs:

  1. Don't compress SSE responses — it buffers your stream into chunky bursts.
  2. Echo Mcp-Session-Id on every MCP request after init, or session state silently breaks.
  3. Propagate traceparent across every layer, or you'll never debug a multi-hop agent failure.

The four real-world stacks:

StackTransportFramingMessage
Chat token streamingHTTP StreamingSSEProvider JSON
MCP (local)stdionewline-delimitedJSON-RPC
MCP (remote)HTTP StreamingJSON or SSEJSON-RPC
Generative UI (A2UI)HTTP StreamingSSEA2UI envelope
保存したスキル