初探 SSE(Server-Sent Events)
初探 SSE(Server-Sent Events)

🔖 文章索引* 🔖
- SSE (Server-Sent Events) 介紹 2. 適合與不適合使用 SSE 的情境 3. *SSE 的侷限
- 建立 SSE 連線 4.1 伺服器端的幾個關鍵 Header 4.2 資料傳輸格式
- long polling, short polling, SSE, websocket 之間有什麼差異
- 參考資料
最近有個專案要從 webSocket 換到 SSE (Server-Sent Events),後端原本用的webSocket 套件是 pusher。此專案是公司的出入口安全系統,同仁早上到公司時,都要在大門先刷識別證,系統就會顯示此員工的部門、職稱、卡機位置、刷卡時間等資訊給警衛看,由於只有需要後端推播給前端,不需要來回溝通,所以 SSE 其實就夠了,同時也輕量許多。
SSE (Server-Sent Events) 介紹

如果把 websocket 比喻成電話,那 SSE 就像是廣播,是一種單向傳輸,讓伺服器主動推送資料到瀏覽器。SSE 的本質是一個持久化的 HTTP 連線,連線會保持開啟狀態,伺服器會不斷地往這個連線裡寫入新的數據,與一般的 HTTP 請求傳完就斷不同。
有以下特點:
- 單向的主動推送
- 基於 HTTP 協議
- 前端用 Web API EventSource 接收
- 如果遇到網路斷線、服務重啟、proxy 中斷等連線斷掉的情境,瀏覽器的 EventSource 會帶上
Last-Event-ID嘗試重新連線,Server 也可以指定斷線後多久要進行重連 - 支援不同事件類型 Type

- 比 webSocket 輕量
適合與不適合使用 SSE 的情境
SSE 適合單向傳輸的場景(sever to client),例如:通知中心、股票即時狀態等。不適合需要大量雙向互動的場景,例如多人遊戲、即時協作應用、聊天室等,這使用 webScoket 更合適

bytebytego
SSE 的侷限
以下舉幾個實務上常見的坑
- 連線數量限制:
在 HTTP/1.1 下,瀏覽器對同一個網域的 TCP 連線數通常是 6 個, 如果是使用 HTTP/2 或 HTTP/3 協定就不會有這問題,這叫做多路復用 (Multiplexing)
什麼算是一個連線數?
如果你在同一個瀏覽器打開了 6 個分頁,每個分頁都連到
example.com接收 SSE 資料,就佔用了 6 個連線,這時如果開了第 7 個分頁,那這個分頁的請求會進入 Pending 狀態,直到前面有連線的分頁被關閉
- 原生 EventSource 加 custom header 很不方便
另外一個侷限是原生的 EventSource 不方便加 custom header,WHATWG 定義的建構子是 new EventSource(url, { withCredentials }) ,沒有像 fetch 可以直接設定 headers 的參數
fetch('/api/user', {
headers: {
Authorization: `Bearer ${token}`,
},
});
// The EventSource interface
interface EventSource : EventTarget {
constructor(USVString url, optional EventSourceInit eventSourceInitDict = {});
readonly attribute USVString url;
readonly attribute boolean withCredentials;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSED = 2;
readonly attribute unsigned short readyState;
// networking
attribute EventHandler onopen;
attribute EventHandler onmessage;
attribute EventHandler onerror;
undefined close();
};
dictionary EventSourceInit {
boolean withCredentials = false;
};
實務上若要驗證,通常用 cookie、session 或 polyfill
polyfill 在這的意義
用第三方套件模擬原生
EventSource,但底層可能用fetch或 XHR,讓其可以支援 custom headers
以下是一些套件整理,大致可以分成兩類:
-
接近
EventSourceAPI 的 polyfill 或 replacement -
不是原生 polyfill,但用
fetch重新實作 SSE client,通常是為了可以 custom headers / POST / retry 控制
- event-source-polyfill (比較老牌的 EventSource polyfill)
- @microsoft/fetch-event-source (不是傳統意義上的 polyfill,而是用
fetch做一個更彈性的 SSE client) - eventsource ( 與 WhatWG 相容的 EventSource client,可以在 Node.js、瀏覽器、Deno、Bun 等環境使用)
- sse.js (EventSource replacement)
3. 反向代理可能會把資料先暫存起來(buffer)
怎麼判定現在資料被 buffer?
判定方式是可以在 Chrome DevTools 的 Network 裡點 SSE 那個 request,接著點 Response Tab。如果資料不是每隔一段時間一筆筆出現,而是延遲後一次出現好幾筆,通常是 buffer 問題。

例如 nginx 如果把回應先暫存起來,前端就不會即時收到資料了,常見解法是請後端在 header 加上 X-Accel-Buffering: no來關閉 buffering,並定期送 comment heartbeat ,透過定期送空訊號,讓長連線不要被當成閒置連線斷掉,同時也幫 server 更快發現 client 已經離線。如同上圖的 : heartbeat,這種以 : 開頭的是 SSE comment,不會觸發 onmessage
// Express
app.get('/api/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // X-Accel-Buffering
res.flushHeaders?.();
// 假設每 15 秒送一次 heartbeat
const heartbeat = setInterval(() => {
res.write(`: heartbeat\n\n`);
}, 15000);
const sendData = (data: unknown) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
req.on('close', () => {
clearInterval(heartbeat);
});
});
⚠️ 在 MDN 的範例中不是使用 : heartbeat comment,而是定時送出 event: ping。這種寫法會被前端監聽到,但因為它會定期讓 SSE 有資料流動,所以也能達到類似 heartbeat 的效果 (完整範例 code)
echo "event: ping\n",
'data: {"time": "' . $curDate . '"}', "\n\n";
建立 SSE 連線
前面提到 SSE 是由 Client Side 發起一個 HTTP request,Server Side 保持這個 response 不結束,持續透過同一個連線把資料推送給 Client Side
伺服器端的幾個關鍵 Header
建立 SSE endpoint 時,伺服器端需要設定幾個關鍵的 Header
Content-Type: text/event-stream(告訴瀏覽器這是一個持續的 Data stream)Cache-Control: no-cache(防止瀏覽器快取,確保即時性)Connection: keep-alive(保持連線不中斷)
資料傳輸格式
SSE 傳回的是純文字,每一個事件區塊結尾都以兩次換行符號 (\n\n) 結尾。有幾個特定的欄位:
data:訊息內容event:自定義事件名稱。前端可以根據不同的事件執行不同的邏輯id:訊息的唯一識別。如果連線中斷,瀏覽器重連時會發送Last-Event-ID給伺服器,伺服器就能補發漏掉的訊息retry:瀏覽器在斷線後,隔多久嘗試重新連線(單位:毫秒)
id: 100 // 唯一識別
event: flight // 事件名稱
data: {"flight":[{...}]} // 訊息內容
// 前端可以用 flight 接收事件
function createSSEConnection(...) {
let eventSource = null;
...
eventSource.addEventListener("flight", (event) => {
console.log(data.percent);
...
});
}
以下附上一份較完整的範例 code:
type SSEOptions<T = unknown> = {
url: string;
withCredentials?: boolean;
onOpen?: (event: Event) => void; // 連線成功時觸發
onMessage?: (data: T, event: MessageEvent) => void; // 瀏覽器收到完整事件後觸發
onError?: (event: Event) => void; // 連線錯誤或中斷時觸發
events?: Record<string, (data: T, event: MessageEvent) => void>;
parseJson?: boolean;
};
export function createSSE<T = unknown>({
url,
withCredentials = false,
onOpen,
onMessage,
onError,
events = {},
parseJson = true,
}: SSEOptions<T>) {
const source = new EventSource(url, {
withCredentials,
});
const parseData = (event: MessageEvent): T => {
if (!parseJson) {
return event.data as T;
}
try {
return JSON.parse(event.data) as T;
} catch {
return event.data as T;
}
};
source.onopen = (event) => {
console.log('[SSE] connected');
onOpen?.(event);
};
source.onmessage = (event) => {
const data = parseData(event);
onMessage?.(data, event);
};
source.onerror = (event) => {
console.error('[SSE] error', event);
onError?.(event);
};
Object.entries(events).forEach(([eventName, handler]) => {
source.addEventListener(eventName, (event) => {
const messageEvent = event as MessageEvent;
const data = parseData(messageEvent);
handler(data, messageEvent);
});
});
return {
source,
close() {
source.close();
console.log('[SSE] closed');
},
getReadyState() {
return source.readyState;
},
};
}
使用方式:
const sse = createSSE({
url: '/api/sse',
onOpen() {
console.log('SSE 連線成功');
},
onMessage(data) {
console.log('收到預設 message:', data);
},
onError() {
console.log('SSE 發生錯誤或正在重連');
},
events: {
newMessage(data) {
console.log('收到 newMessage:', data);
},
notification(data) {
console.log('收到 notification:', data);
},
},
});
建立 SSE 連線的核心流程圖:
原本想自己畫流程圖,但發現 AI 畫的不錯,那就直接用吧XD

PS. 若要中斷連線,可使用 close() 內建函式
long polling, short polling, SSE, websocket 之間有什麼差異
polling: 一種資料同步機制。客戶端(如網頁)會設定固定的時間間隔,不斷向伺服器發出請求,詢問是否有新資料。可以分成 Short Polling 和 Long Polling 兩種

bytebytego
從上圖中可以看到不管哪一種 Polling 一開始都是由 Client Side 主動問 Sever Side,而 Short Polling 和 Long Polling 的差異就在發送 request 的頻率
- Short Polling:每隔一段時間發送 HTTP request
- Long Polling:請求一次後,Sever Side 有資料才回應;回應完 Client Side 再發起下一次請求
short Polling
setInterval(async () => {
const res = await fetch('/api/notifications');
const data = await res.json();
console.log(data);
}, 3000);
若想像成對話,大概是像以下吧XD
Client: 有新資料嗎?
Server: 沒有喔
3 秒後…
Client: 有新資料嗎?
Server: 沒有喔
3 秒後…
Client: 有新資料嗎?
Server: 有喔,傳給你
優點:簡單明瞭 缺點:浪費 request,而且更新有可能不夠即時
Long Polling
async function poll() {
const res = await fetch('/api/events');
const data = await res.json();
console.log(data);
poll(); // 收到後立刻發下一次
}
poll();
(腦補對話)
Client: 有新資料嗎?
Server: .....
過了 10 秒…
Server: 有資料了,傳給你
Client: 收到,馬上再問下一次
Client: 有新資料嗎?
優點:比 Short Polling 即時,也比較不浪費 request,但本質上還是一次 request 對一次 response
SSE
開一條 HTTP 長連線(Persistent Connection),Server Side 持續回傳資料
const source = new EventSource('/api/events');
source.onmessage = (event) => { ... };
(腦補對話)
Client: 我要訂閱事件
Server: 好,這條線保持開著
Server: data: 第一筆資料
Server: data: 第二筆資料
Server: data: 第三筆資料
WebSocket
Client Side 和 Server Side 建立一條持久連線(Persistent Connection),雙方都可以主動送資料
const socket = new WebSocket("ws://localhost:3000");
socket.onmessage = (event) => { ... };
socket.onopen = () => {
socket.send("hello");
};
(腦補對話)
Client A: connect
Server -> Client A: connected
Client B: connect
Server -> Client B: connected
Client A -> Server: Hello, I'm Client A
Server: 收到 Client A 傳來的訊息
Server -> Client B: (傳送訊息給 Client B)
Client B: 收到新訊息:「A: Hello, I'm Client A」
Client B -> Server: Hi~~
Server: 收到 Client B 傳來的訊息
Server -> Client A: (傳送訊息給 Client A)
Client A: 收到新訊息:「B: Hi~~」
各種使用情境整理:
- 如果只是偶爾更新,例如每 30 秒刷新後台資料 -> Short Polling
- 如果想做通知,但環境不方便支援 SSE -> Long Polling
- 如果是 AI 串流、任務進度、log、通知,且環境支援長連線串流 -> SSE
- 如果是聊天室、遊戲、多人協作、即時雙向互動 -> WebSocket
參考資料
메타데이터
- post_id
- 48b8d43e559b
- slug
- 初探-sse-server-sent-events-48b8d43e559b
- url
- https://medium.com/@hanforworkandlife/%E5%88%9D%E6%8E%A2-sse-server-sent-events-48b8d43e559b
- canonical_url
- https://medium.com/@hanforworkandlife/%E5%88%9D%E6%8E%A2-sse-server-sent-events-48b8d43e559b
- author_url
- https://medium.com/@hanforworkandlife
- status
- ok
- fetched_at
- 2026-06-24 04:09:36