sse场景下必须禁用readtimeout和writetimeout,仅保留idletimeout和readheadertimeout,并在handler内用context或time.afterfunc实现语义化超时与资源清理。

Go HTTP Server 的 ReadTimeout/WriteTimeout 会直接杀死 SSE 连接
如果你正在用 Go 实现 Server-Sent Events(SSE)或其它需要长连接的接口(比如 WebSocket 升级前握手、流式 JSON),却频繁遇到连接被断开、前端反复重连,大概率是因为 http.Server 的 ReadTimeout 或 WriteTimeout 在起作用。
这两个字段是连接级全局超时:从 TCP 连接建立或请求头接收开始计时,一旦超时就无条件关闭 net.Conn。而 SSE 要求连接保持数分钟甚至更久,10 秒超时等于直接宣告失败。
- 禁用
ReadTimeout和WriteTimeout是必须的第一步,不是可选项 - 不要试图调高它们(比如设成 5 分钟)——这会掩盖问题,且无法应对空闲连接探测、客户端意外掉线等真实场景
-
IdleTimeout(Go 1.8+)可以保留,它只影响“完全没数据收发”的连接,对持续写入 event: / data: 的 SSE 是安全的
用 context 或 time.Timer 在 Handler 内做语义化超时
真正的超时控制应该落在业务逻辑层:比如“最后一次写入后 30 秒无新事件则断开”,或“总存活时间不超过 10 分钟”。这类判断无法由连接层完成,必须在 http.Handler 内实现。
- 推荐用
time.AfterFunc启动一个延迟清理 goroutine,每次写入成功后Reset()它 - 避免直接用
context.WithTimeout包裹整个 Handler —— 它会在超时后关闭ResponseWriter,但底层连接可能还在,容易导致write: broken pipe - 务必在 defer 中显式调用
timer.Stop(),否则 timer 会泄漏并持续触发 - 如果依赖外部 channel(如消息队列)推送事件,记得用
select+ctx.Done()做退出信号监听,防止 goroutine 挂住
Transport 层超时和 Server 层超时不能混为一谈
有人看到 http.Transport 里有 ResponseHeaderTimeout、IdleConnTimeout 就想照搬到 http.Server,这是典型混淆。
http.Transport 是客户端行为,http.Server 是服务端行为;前者控制“我作为客户端怎么连别人”,后者控制“我作为服务端怎么管进来的人”。SSE 场景下,你只需要管好自己的 http.Server 配置和 Handler 行为。
-
http.Server.IdleTimeout可设(例如 60s),它只 kill 真正空闲的连接,不影响活跃 SSE -
http.Server.ReadHeaderTimeout可设(例如 5s),它只限制请求头读取阶段,对已进入 Handler 的长连接无影响 - 别碰
ReadTimeout/WriteTimeout,文档里明确写了它们与 keep-alive 不兼容
最容易被忽略的 Goroutine 泄漏点
一个看似正确的 SSE Handler,上线后内存缓慢上涨、连接数持续增加,往往不是因为超时没设,而是清理逻辑缺失。
- 每个 SSE 连接对应至少一个长期运行的 goroutine,它必须有明确的退出路径
- 不要只依赖客户端断开触发
conn.Close()—— 移动网络切换、NAT 超时、代理中断都可能导致连接静默丢失 - 建议在每次写入前加
if !h.w.HeaderWritten { return }判断,避免向已关闭的 writer 写数据引发 panic - 用
net/http/pprof定期看 goroutine profile,搜索sseHandler或你的 handler 名,确认数量是否稳定











