Server-sent events 筆記

什麼是 Server-sent events (SSE)

Server-sent events (簡稱 SSE) 是一種利用持久連線,讓用戶端可以透過送出一個請求後,持續得到伺服器端回應的模式。

用戶端會在請求標頭中使用 Accept: text/event-stream 來發起 SSE 連線,接著伺服器端透過在回應標頭加上 "Content-Type: text/event-stream" 來預告接下來的回應會是串流的形式。

看到 "Content-Type: text/event-stream" 可能會想說「啊!這不就是 streaming response 嗎?」,沒錯,事實上 SSE 不是什麼新技術,它可以說是一個有固定格式的 streaming response。

直接看 raw bytes

與其看規格書,不如直接抓一個真實的 SSE 回應下來看。這裡打自建 Gateway,因為 LLM 的 streaming API 就是一個最適合的應用場景:

1
2
3
4
5
6
$ curl -N -D headers.txt \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gemini-3.5-flash","stream":true,
"messages":[{"role":"user","content": "給我一首五言絕句"}]}' \
https://<gateway>/api/v1/chat/completions

來看回應標頭:

1
2
3
4
5
HTTP/2 200
server: nginx/1.31.5
content-type: text/event-stream; charset=utf-8
x-litellm-call-id: 30c459ed-0285-4835-b658-cb6b2fd6f851
vary: Accept-Encoding

重點來了:content-type: text/event-stream。伺服器端靠這個 Content-Type 告訴用戶端「接下來的 body 請用 SSE 的格式去解析,而且不要等我關閉連線」。

再來看後面的回應:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"這是一首非常經典且家","role":"assistant"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"喻戶曉的唐詩五言絕句:\n\n### 《登鸛雀樓》—— 王之渙\n\n**白日"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"依山盡,**\n**黃河入海流。**\n**欲窮千里目,**\n**"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"更上一層樓。**\n\n---\n\n### 【簡析】\n這首詩前兩句寫景,描繪"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"了夕陽依山下落、黃河奔流入海的壯麗遼闊景象;後兩句抒"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"懷,道出了「只有站得高,才能看得遠」的人生意境與哲理,既有磅"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"礴的氣勢,又富含積極向上的力量。"}}],"vertex_ai_grounding_metadata":[],"vertex_ai_url_context_metadata":[],"vertex_ai_safety_ratings":[],"vertex_ai_citation_metadata":[]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"finish_reason":"stop","index":0,"delta":{}}]}

data: {"id":"NGWfaqKKDsuI5uAPi-LwUQ","created":1788831032,"model":"gemini-3.5-flash","object":"chat.completion.chunk","choices":[{"index":0,"delta":{}}],"usage":{"completion_tokens":774,"prompt_tokens":9,"total_tokens":783,"completion_tokens_details":{"reasoning_tokens":627,"text_tokens":147},"prompt_tokens_details":{"text_tokens":9}}}

data: [DONE]

這就是 SSE 的全部了,可以看到每個 SSE 回應都是 data 開頭的 object。

事件內容

事件串流是個簡易的文字資料串流,內容必須以 UTF-8 格式編碼。在事件串流中,不同的訊息以一對換行符號做區隔。
SSE 規格只定義了四個欄位,全部都是選用的:

欄位 用途
data: 事件內容。同一個事件可以有多行 data:,用戶端會用 \n 把它們接起來
event: 事件類型。用戶端可以針對不同類型註冊不同的 handler,不寫的話預設是 message
id: 事件 ID。用戶端會記住最後一個 ID,斷線重連時自動帶在 Last-Event-ID 標頭裡
retry: 告訴用戶端斷線後要等幾毫秒才重連,單位 ms

另外可以用 : 開頭的當作是註解,會被用戶端忽略。實務上常拿來當 heartbeat,每隔一段時間送一個 : ping\n\n 避免中間的 proxy 因為 idle timeout 把連線砍掉。

它要解決什麼問題

一般的 Request-Response 模式是,用戶端和伺服器端一來一回,在某些情況下伺服器需要耗時回傳大量的內容,這種傳統模式就會讓用戶端等太久造成體驗不佳。

SSE 就是為了解決這種問題而設計的,利用 streaming 的形式,只要內容已經準備好就可以先送回用戶端,而 AI Chat 的模式就是最適合使用的情境:

使用者輸入 prompt,而 LLM 會持續產生內容,透過 SSE 把內容輸出到用戶端,搭配前端使用打字機效果,看起來就很像有個人在電腦後面和你對話。

除此之外 SSE 還有一個好處:斷線重連

一般的 streaming response 斷了就是斷了,用戶端不是認分重來,就是要自己實作一套續傳機制。SSE 規定伺服器用 id: 標記每個事件,用戶端記住最後一個 ID,斷線後自動重連並在 Last-Event-ID 標頭帶回去,伺服器就能知道要從哪裡接下去送回應。

和 Request、WebSocket、Polling 這些模式有什麼區別

一般 Request / Response

一來一回,連線用完就關。問題是使用者在伺服器處理的整段時間內什麼都看不到。

Polling 輪詢

用戶端定時去問。實作最簡單,但先天有兩個問題:延遲取決於輪詢間隔(間隔 3 秒代表最慢 3 秒才看得到),而大部分請求都是空手而回,白白消耗連線與伺服器資源。

Long Polling 長輪詢

伺服器收到請求後不馬上回,一直 hold 到有資料(或 timeout)才回,用戶端收到後立刻再發一個。延遲比 polling 好很多,但每收一次資料就要重建一次連線,而且伺服器要 hold 住大量未完成的請求。這是 SSE 普及前的主流做法。

WebSocket

透過 101 Switching Protocols 把 HTTP 連線升級成 WebSocket 協定,之後就是全雙工的雙向通道。功能最強,可以讓用戶端和伺服器端即時地交換資訊。

SSE

一個請求換來持續不斷的回應。單向(只有伺服器能推),但斷線重連與續傳是內建的。

綜合比較

Request Polling Long Polling SSE WebSocket
方向 單次一問一答 用戶端拉 用戶端拉 伺服器推(單向) 雙向
底層協定 HTTP HTTP HTTP HTTP 從 HTTP 升級成 ws/wss
連線數 每次一條 每次一條 每次一條 一條,長期持有 一條,長期持有
即時性 差(取決於間隔) 中等 最好
無效請求
自動重連 天生就是重複請求 天生就是重複請求 瀏覽器內建 要自己寫
斷線續傳 自己帶游標 自己帶游標 Last-Event-ID 內建 要自己寫
資料格式 任意 任意 任意 只能是 UTF-8 文字 文字或二進位
瀏覽器 API fetch fetch + timer fetch + 迴圈 EventSource WebSocket
自訂標頭 可以 可以 可以 EventSource 不行 不行(握手時)
連線數上限 HTTP/1.1 同網域 6 條 不受此限
適合場景 一般 API 低頻更新 過渡方案 LLM 串流、通知、進度、股價 聊天、遊戲、協同編輯

小結

學習 SSE 最大的收穫是又更了解 HTTP、TCP 等運作的方式了。之前一直搞不懂:TCP 本來就會把內容切成好幾段持續傳送,這不就是一種 streaming 嗎?那為什麼還要分 application/jsontext/event-stream

原來差異只是用戶端針對不同 Content-Type 的處理不同,看是要立即處理,還是結束後才處理的差別。

參考資料

  1. https://developer.mozilla.org/zh-TW/docs/Web/API/Server-sent_events/Using_server-sent_events
  2. https://html.spec.whatwg.org/multipage/server-sent-events.html
  3. https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering

Server-sent events 筆記
https://weiblog.me/2026-09-08/2026-server-sent-events-notes/
Author
wei
Posted on
September 8, 2026
Licensed under