
WebSocket 是个好东西。真的,我没在阴阳。
2008 年诞生,2011 年标准化,它解决了 HTTP 协议的一个根本缺陷:服务端不能主动给客户端发消息。有了 WebSocket,实时推送、在线协作、即时通讯——这些以前要靠轮询或长轮询才能勉强实现的功能——一下子变得自然了。
但问题恰恰在这里。
因为 WebSocket 解决了"实时推送"这个痛点,它变成了一个默认选项。看到"实时"两个字,很多团队不加思考就选了 WebSocket。就像看到"性能优化"就想到加缓存一样——不是不对,是太粗糙了。
大多数只需要单向推送的场景,WebSocket 是过度设计。而且这不是技术判断,是成本账。
一、你的 WebSocket 可能是个"豪华车"
我之前在一个后台系统项目里做过推送功能。
需求很简单:服务端任务状态变更时,通知前端更新界面。典型的管理后台场景——任务队列进度、系统告警、数据同步状态。没有一个场景需要前端往服务端发消息。
你猜我们选了什么?
WebSocket。
理由当时觉得非常充分:“实时推送嘛,当然用 WebSocket。稳定、成熟、大家都在用。”
然后踩坑就开始了。
首先是连接维护。 WebSocket 建连要走 6 次消息交换——TCP 三次握手 + HTTP 升级 + WS 帧交换。而且建连只是开始,连接建立后每隔几十秒要发一次心跳,否则代理层(Nginx、ALB 等)默认超时会断开连接。每个连接服务端都要维护状态。前端还需要自己实现重连逻辑——因为 WebSocket 没有内置重连。你需要在客户端维护一个完整的连接状态机:连接中、已连接、断开中、重连中——每个状态都要处理边界情况。
基础设施那边也不省心。 负载均衡器要开粘性会话,不然客户端换了个实例连接就断了。Nginx 要配 proxy_set_header Upgrade 和 proxy_set_header Connection "upgrade"。AWS ALB 要开 WebSocket 支持。防火墙要放行 WebSocket 端口。如果是公司内部网络,可能还要申请安全策略变更——一套流程下来两三天。
运维更烦人。 连接断开的场景太多了——浏览器休眠、网络切换、代理超时。每断一次,前端要重新走一遍握手流程。而且 WebSocket 的调试比 HTTP 复杂得多,出了问题上 Wireshark 抓包才能看清。HTTP 接口出问题,curl 一下就知道。WebSocket 呢?你得装专门的 WebSocket 客户端工具,还要理解帧结构。
回头看,这个场景的实际需求只有一条:“服务端有更新时通知前端”。纯单向。这不只是管理后台的问题——2025-2026 年增长最快的实时通信场景,AI 流式输出,也是这个模式:你发一个请求,服务端把 Token 一条一条吐回来。同样是纯单向。
而我们选择了一个为"双向实时通信"设计的协议,为不需要的能力付了全价。
这不是个例。我见过太多团队掉进同一个坑里:
- 一个内部运营后台,用 WebSocket 做通知推送——需求就是"任务完成时弹个 toast"
- 一个数据看板,用 WebSocket 推送图表更新——需求就是"新数据来了刷新图表"
- 一个告警系统,用 WebSocket 推送告警消息——需求就是"有告警时显示在屏幕上"
- 一个 DevOps 面板,用 WebSocket 推送部署日志——需求就是"日志一行一行出来"
它们大部分是单向的——至少单向占了 90% 以上的流量。

你可能会说:“用 WebSocket 也没什么大问题吧?反正也能工作。” 对,能工作。就像一个超市用冷藏车运矿泉水一样——能运到,但成本不对。
二、WebSocket 的"奢侈税"
单向推送用 WebSocket,到底多花了多少冤枉钱?我们来算一笔账。
建连成本
WebSocket 建连需要 2 个网络往返(RTT),而 SSE 只需要 1 个。
WebSocket 建连:TCP SYN → SYN-ACK → ACK → HTTP Upgrade → 101 Switching → 数据
SSE 建连: HTTP GET → 200 OK + 流式数据
建连阶段,WebSocket 比 SSE 多 1 个 RTT + 约 400 字节的 HTTP 头。这些额外头包括:Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key(16 字节随机值)、Sec-WebSocket-Version: 13。服务端回复的 101 Switching Protocols 还包含 Sec-WebSocket-Accept 头。

可能你会说:“才 1 个 RTT,不在乎。” 但连接不是一次性的——每次断线重连,你都要多付这 1 个 RTT。如果客户端网络不稳定(移动端尤其),一天断连十几次,累计的成本就很可观了。
维护成本
WebSocket 没有内置心跳,但生产环境中几乎每个实现都要加。具体间隔取决于代理层配置(Nginx 的 proxy_read_timeout 默认 60s,ALB 默认 350s),通常每 30-60 秒一次 Ping/Pong,双向交换。按 30 秒间隔算,每小时每连接约 120 次心跳。
SSE 也不需要心跳——但生产环境中同样需要维持连接。区别在于 SSE 的心跳是服务端单向发送的注释行(: keepalive\n\n),不需要客户端响应。这比 WebSocket 的 Ping/Pong 省了一半的带宽和一次额外的处理开销。
从协议状态的角度看,差距更大。WebSocket 服务端需要为每个连接维护的状态比 SSE 复杂得多——TCP 连接状态、协议帧缓冲、掩码键、应用层会话、粘性会话绑定。大致来说,每个连接约 2-5 KB 的协议层状态开销。而 SSE 走标准 HTTP 响应,每个连接约 0.5-1 KB——少了约 75%。在千级连接规模下,这意味着几 MB 到十几 MB 的内存差异。对单台服务器来说不算大,但如果你有几十台实例,差距就出来了。
还有一个很多人忽略的点:WebSocket 的粘性会话要求意味着你无法使用轮询(round-robin)负载均衡。如果你的某个实例挂了,上面维护的所有 WebSocket 连接都会中断。而 SSE 基于标准 HTTP,任何实例都可以接管。
这就是差距。
数据说话
光算理论太虚。我在本地跑了一组 benchmark:1000 个并发连接,每个连接推送 100 条消息,模拟典型的管理后台推送场景。
服务端用 Go 实现,SSE 走标准 text/event-stream,WebSocket 走简易文本帧(无 mask,纯服务端推送)。两者推送相同内容(JSON 格式,每条约 200 字节),客户端用并发 HTTP 请求模拟。
测试环境:Apple M4 Pro / Go 1.26.4
| 指标 | SSE | WebSocket | 差异 |
|---|---|---|---|
| 耗时 | 1125ms | 2324ms | WebSocket 慢 106% |
| 内存分配 | 75MB | 93MB | WebSocket 多 23% |
| 分配次数 | 552K | 730K | WebSocket 多 32% |
在同等推送量的单向场景下,WebSocket 的耗时是 SSE 的两倍多,内存分配多近四分之一。

这不是说 WebSocket 性能差——它只是在做它该做的事:维护双向通道、支持帧级控制、处理掩码。问题是,这些能力在你的场景中用不上。你为"双向"付了钱,但只用了"单向"。
有人可能会说:“那用 WebSocket 做单向推送,加上心跳,成本也没高到哪去吧?”
算一笔账。假设你有 1 万个在线连接,每个连接每 30 秒一次心跳。WebSocket 每小时需要处理约 120 万次 Ping/Pong 交换。这些心跳本身也是资源——CPU 处理帧、网络带宽、中断处理。SSE 的注释行虽然也占带宽,但不需要客户端响应,服务端少了一半的工作。
做会议机器人基础设施的 recall.ai 在 2024 年分享过他们的经验:WebSocket 导致的额外 CPU 开销让他们每年多付了约 $100 万的 AWS 费用。通过用共享内存替代 WebSocket 做进程间通信,他们将 CPU 使用率降低了 50%,节省了超过 $100 万/年的 AWS 成本。虽然 recall.ai 的优化方案不是简单的 SSE 替换,但故事的核心是一样的——你为 WebSocket 的"双向"能力付出的隐性成本,远比你想的大。
三、AI 来了,SSE 的春天来了
如果 SSE 只是"更轻量的推送方案",它不会成为 2026 年的热门话题。真正让 SSE 翻身的,是 AI。
想想看,LLM 的流式输出是什么样的?你发一个 Prompt 过去,服务端把 Token 一个接一个吐回来。这是一次 POST 请求 + 持续的、单向的 Token 流。
这就是 SSE 最擅长的模式。
OpenAI 的 Chat Completions API 用 SSE,Anthropic 的 Messages API 用 SSE,Google Gemini 的 StreamGenerateContent 也用 SSE。这不是巧合——在"一问多答"的场景下,SSE 是天然的匹配。具体来说:
- OpenAI:
/v1/chat/completions带上stream: true,返回的就是text/event-stream格式。每条事件包含一个data: {"choices":[{"delta":{"content":"token"}}]},最后以data: [DONE]结束 - Anthropic:
/v1/messages带上stream: true,返回 SSE 流。事件类型包括message_start、content_block_delta、message_stop等,粒度比 OpenAI 更细 - Google Gemini:
/v1/models/{model}:streamGenerateContent,同样是 SSE 格式
而 WebSocket 在这个场景下反而尴尬:你需要先建立双向通道,然后告诉服务端"我只收不发",然后维持一个不需要双向能力的双向连接。相当于你买了一辆能跑 300 码的跑车,每天只开 30 码去菜市场。

AI 流式输出不是一个边缘场景——从 2023 到 2026 年,OpenAI、Anthropic、Google 的流式 API 调用量增长了数个数量级,它已经成为实时通信中增长最快的场景之一。 大多数 AI 应用的背后,都有一条 SSE 数据流。Claude、Gemini、通义千问、文心一言——所有主流 LLM 的流式 API 都基于 SSE。
这个趋势把 SSE 从一个"有人用但不多"的协议,推到了基础设施的位置。就像 HTTP/2 普及让长连接不再是问题一样,AI 普及让 SSE 从一个"替代方案"变成了"默认方案"。
如果你正在开发一个 AI 应用——不管是大模型对话、代码生成、还是文档总结——你大概率已经在用 SSE 了,只是你可能没意识到它叫 SSE。你的前端代码里可能早就写好了 onmessage 回调,只是你以为那是 WebSocket。
四、HTTP/2 帮 SSE 补上了最后一课
SSE 有一个历史问题:HTTP/1.1 下,每个源最多 6 个并发连接。如果一个页面需要同时打开多个 SSE 通道(比如一个推通知、一个推数据更新),超过 6 个后新的连接会被阻塞。
这是 SSE 最大的硬伤,也是很多人放弃它的理由。
但这个限制在 HTTP/2 时代已经不存在了。
HTTP/2 的多路复用(Multiplexing)允许在单个 TCP 连接上承载最多 100 个并发流。每个 SSE 连接对应一个流,帧(frame)在连接上交错传输。
换句话说,HTTP/1.1 下你需要 6 个 TCP 连接来跑 6 个 SSE 通道,HTTP/2 下 1 个 TCP 连接就够了——而且能跑 100 个。
HTTP/1.1 + SSE:
浏览器 ←─── TCP 连接 1 (SSE 通道 1) ───→ 服务器
←─── TCP 连接 2 (SSE 通道 2) ───→ 服务器
←─── ...(最多 6 个,第 7 个排队等待)
HTTP/2 + SSE:
浏览器 ←─── 1 个 TCP 连接 ───→ 服务器
├── 流 1 (SSE 通道 1)
├── 流 2 (SSE 通道 2)
├── 流 3 (SSE 通道 3)
├── ...(最多 100 个流)
不仅如此,HTTP/2 还带来了头部压缩(HPACK)。HTTP/1.1 下每个 SSE 响应都重复发送相同的 HTTP 头(Content-Type、Cache-Control 等),HPACK 压缩后这些重复头的开销几乎为零。
2026 年,Chrome、Firefox、Safari 15+ 都已默认启用 HTTP/2。大部分生产环境的反向代理(Nginx 1.9+ 需主动开启 http2 配置、ALB、Cloudflare)都支持 HTTP/2 转发。不过要注意一个边界条件:浏览器支持 HTTP/2 不等于连接一定使用 HTTP/2——如果客户端网络环境受限(如企业代理拦截 HTTP/2 升级、CDN 回源到后端的连接仍是 HTTP/1.1),SSE 的连接数限制仍然存在。生产环境中建议确认从客户端到代理的整条链路都支持 HTTP/2。

这意味着:如果你已经在用 HTTP/2,SSE 的连接数限制对你来说不存在。
这是一个"补课式"的解决——传输层升级让这个 bug 不再是问题。就像 IPv4 地址不够用,不是改了 IP 协议,是 NAT 帮忙扛了十几年。至于 HTTP/3(基于 QUIC),它对 SSE 更友好——0-RTT 建连、连接迁移——但 2026 年的生产环境,HTTP/2 已经够用了。
五、什么时候选什么——决策框架
说了这么多,到底怎么选?
我总结了一个四维决策框架:通信模式 → 可靠性要求 → 基础设施 → 性能边界。
第一步:你需要双向通信吗?
这是最关键的一问。如果答案是"不需要"或"很少需要"(偶尔发个指令,大部分时间在收推送),SSE 是更好的选择。
单向推送用 SSE,双向交互用 WebSocket。就这么简单。
判断标准:数一下你的应用里,服务端发消息给客户端的次数和客户端发消息给服务端的次数。如果比例超过 10:1(收 10 条才发 1 条),SSE 值得考虑。
第二步:你需要多高的可靠性?
如果每条消息都必须送达、顺序必须保证、失败必须重试——这是 WebSocket 的领地,它有标准的 ACK 机制。金融交易、实时协作编辑这类场景需要这种保证。WebSocket 的应用层可以自定义确认帧,服务端知道"客户端确实收到了这条消息"。
如果丢几条没关系、断线后自动恢复就行——SSE 的 EventSource 内置重连,大部分场景够用。状态更新、进度推送、通知提醒这类场景,SSE 的可靠性已经足够。EventSource 断线后会自动重连,并携带 Last-Event-ID 头告诉服务端"我从哪里开始重连"。
有一个中间方案值得注意:SSE + 定期快照。服务端每隔一段时间(如 30 秒)发一条完整状态快照,客户端即使丢了几条增量事件,下次快照来了也能恢复。这比 WebSocket 的逐条 ACK 更轻量,对大部分"实时展示"场景已经足够。
第三步:你的基础设施长什么样?
| 对比项 | SSE | WebSocket |
|---|---|---|
| 负载均衡 | 标准 HTTP(无需特殊配置) | 需粘性会话(sticky sessions) |
| 防火墙 | 标准 HTTPS 端口(443) | 可能被企业防火墙阻断 |
| 客户端实现 | 浏览器内置 EventSource | 需引入第三方库(socket.io、SockJS 等) |
| 代理兼容性 | 天然兼容(标准 HTTP 流) | 需额外配置(Nginx Upgrade、ALB WebSocket 支持) |
从基础设施角度看,SSE 在几乎所有维度上都更"便宜"。如果你的团队不大、运维能力有限,SSE 的低维护成本是实实在在的优势。
第四步:你有性能方面的特殊要求吗?
- 需要传输二进制数据?→ WebSocket(SSE 需要 Base64 编码,增加 33% 开销)
- 需要超低延迟(<50ms)?→ WebRTC / WebTransport(还在等 Safari 默认启用)
- 需要在 HTTP/1.1 下开大量推送通道?→ 合并为单个 SSE 连接,用
event字段区分
决策速查
需要双向通信? → 是 → 需要超低延迟或 P2P?→ 是 → WebRTC / WebTransport
→ 否 → WebSocket
→ 否 → 需要传输二进制?→ 是 → WebSocket
→ 否 → 需要自定义请求头鉴权?→ 是 → fetch + ReadableStream
→ 否 → 浏览器支持 EventSource?→ 是 → SSE
→ 否 → 长轮询
核心判断:大多数实时推送场景不需要 WebSocket。这不是技术判断——是成本账。

六、生产陷阱与救火指南
选了 SSE 不代表万事大吉。生产环境中 SSE 有几个常见的坑,踩一个就够你折腾半天。
陷阱 1:Nginx 缓冲
这是 SSE 最常见的生产 bug。Nginx 默认缓冲响应体,SSE 的流式数据会被缓存后才一次性推给客户端。表现是 EventSource 连接正常,但数据"卡住"——等了半天突然一次性收到大量数据。
解法:设置响应头 X-Accel-Buffering: no,或者在 Nginx 配置中关掉 proxy_buffering。
location /sse {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
}
如果后端是 Go,在 http.ResponseWriter 里设置头:
w.Header().Set("X-Accel-Buffering", "no")
陷阱 2:反向代理超时
SSE 是长连接,但反向代理有默认超时。AWS ALB 默认 60s,Cloudflare 默认 100s,Nginx proxy_read_timeout 默认 60s。
解法:把超时设大(如 3600s),同时在应用层每 30-60s 发一条 : keepalive\n\n 注释行刷新超时计数器。
location /sse {
proxy_pass http://backend;
proxy_read_timeout 3600s;
proxy_buffering off;
}
Go 服务端的心跳:
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
go func() {
for {
select {
case <-ticker.C:
fmt.Fprintf(w, ": keepalive\n\n")
flusher.Flush()
case <-r.Context().Done():
return // 客户端断开,安全退出
}
}
}()
陷阱 3:浏览器连接数限制
HTTP/1.1 下最多 6 个 SSE 连接。虽然 HTTP/2 解决了这个问题,但如果你还在用 HTTP/1.1:
解法:把多个推送通道合并为单个 SSE 连接,用 event 字段区分不同类型。
const evtSource = new EventSource("/sse/all");
evtSource.addEventListener("notification", (e) => { /* 处理通知 */ });
evtSource.addEventListener("data_update", (e) => { /* 处理数据更新 */ });
陷阱 4:EventSource 的羊群效应
EventSource 在连接断开时会自动重连。服务端宕机恢复后,大量客户端同时重连——这就是羊群效应(thundering herd),可能导致服务端再次过载。
解法:服务端实现指数退避,或者用 fetch + ReadableStream 替代 EventSource 来精细控制重连策略。
async function createSSEWithBackoff(url, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按 SSE 协议分割:每条消息以 \n\n 结尾
const parts = buffer.split("\n\n");
buffer = parts.pop(); // 保留未完成的部分
for (const part of parts) {
for (const line of part.split("\n")) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") return;
console.log("收到事件:", JSON.parse(data));
}
}
}
}
return;
} catch (err) {
const delay = Math.min(1000 * Math.pow(2, i), 30000);
await new Promise(r => setTimeout(r, delay));
}
}
}
生产环境 Checklist
- Nginx 关掉 proxy_buffering 或设置
X-Accel-Buffering: no - 反向代理超时设到 3600s 或以上
- 应用层心跳(每 30-60s 发
: keepalive) - 使用 HTTP/2 避免连接数限制
- 优雅关闭处理(部署时旧进程不立刻断开连接)
- 监控连接数,异常增长及时告警
- 考虑用
fetch+ReadableStream替代 EventSource(更灵活)
回到开头
WebSocket 是个好东西。它解决了实时通信的核心问题,是双向交互场景的最佳选择。
但大部分实时推送场景不需要双向。
SSE 不是 WebSocket 的"替代品"——它是不同场景的"正确选择"。WebSocket 是"我能做所有事",SSE 是"我只做一件事,但做得更好"。
选技术不是选最厉害的,是选最合适的。下次有人跟你说"我们需要 WebSocket",先问一句:“我们真的需要双向吗?”
你可能会发现,答案比你想象的更简单。
原文发布于 止语Lab
附录:实验代码和原始数据
本文 1 组 Benchmark 实验的代码和原始数据已开源:
GitHub:zhiyulab-evidence/sse-vs-websocket
benchmark/:SSE vs WebSocket 1000 并发推送 Benchmark(Go 实现,可独立运行)
每个子目录都有独立 README,说明如何复现。二进制编译产物不入库,跑实验前自己 go build。