readtimeout覆盖tcp连接建立完成(含tls握手)后读取完整http请求(请求行、全部请求头及请求体)的全过程,不包含accept等待和客户端初始空闲期;它与readheadertimeout正交共存,前者防慢body,后者专防慢header。

ReadTimeout 从 TCP 连接建立完成(含 TLS 握手)开始计时,到服务器读完全部请求头和请求体为止——它不是只管 header,也不是只管建连,而是覆盖“接收完整 HTTP 请求”全过程。
ReadTimeout 管什么、不管什么
它明确包含:net.Conn 建立后的 TLS 握手耗时、HTTP 请求行、所有请求头、以及整个请求体(如 POST body)的读取时间。一旦超时,连接会被立即关闭,http.Server 不会继续读后续字节。
它明确不包含:accept() 系统调用等待新连接的时间(那是操作系统层面的 backlog 队列问题)、客户端在建连后故意慢速发送数据(如 slowloris 攻击)的初始空闲期(这个需靠 ReadHeaderTimeout 或中间件拦截)。
常见误判现象:read tcp 10.0.1.2:8080->10.0.3.4:54321: i/o timeout 报错看似是底层 read,实则是 ReadTimeout 触发后对连接调用了 SetReadDeadline,最终由内核返回该错误。
ReadTimeout 和 ReadHeaderTimeout 的分工必须明确
这两个字段可以共存,但作用阶段不同,不能互相替代:
-
ReadHeaderTimeout:仅限制“请求行 + 所有 header 行”的读取时间,典型值为2 * time.Second。它在连接建立后立刻启动,一旦超时就直接关闭连接,不等 body。 -
ReadTimeout:从连接建立完成起算,覆盖 header + body 全流程,典型值为5 * time.Second或更长(如上传大文件时需放宽)。它允许 header 快速到达后,body 可以慢慢传。 - 若只设
ReadTimeout而不设ReadHeaderTimeout,攻击者可在 header 阶段逐字节发送(如每 10 秒发一个字符),让连接长期占用而无法被及时释放。
为什么不能只靠 context.WithTimeout 在 handler 里控制
Handler 内部用 context.WithTimeout(r.Context(), ...) 只能中断业务逻辑(如 DB 查询、RPC 调用),但无法阻止以下情况:
- 客户端已断开,但服务端还在阻塞读 body(此时
r.Body.Read()仍在等数据,ctx.Done()不会唤醒它) - 客户端缓慢发送 10MB body,handler 已超时返回,但
http.Server仍会继续读完剩余 body(浪费 CPU 和内存) - 连接卡在 TLS 握手未完成,
r.Context()甚至还没创建出来
也就是说:ReadTimeout 是连接层硬性熔断,context 是业务层软性退出——前者必须存在,后者只是补充。
配置建议与易踩坑点
生产环境应显式设置 ReadTimeout,且值需匹配接口语义:
- 纯 JSON API(无 body 或小 body):
ReadTimeout: 5 * time.Second - 文件上传接口(body 可达几十 MB):
ReadTimeout: 30 * time.Second,并搭配ReadHeaderTimeout: 2 * time.Second防 header 慢发 - 绝对不要留空或设为 0 —— Go 默认值是 0,即无限等待
- 避免和
WriteTimeout设成相同值:比如都设10 * time.Second,会导致客户端在写完请求后立刻开始倒计时写响应,实际可用时间被压缩
最常被忽略的是:它和 ReadHeaderTimeout 是正交配置,不是二选一;漏掉任一,都可能让慢请求或恶意连接长期滞留。











