· via dev.to (home feed)
Why SSE streams break in Go production: illegal HTTP/2 headers and default write timeouts
A dev.to post walks through two production SSE failures in Go: a hop-by-hop header forbidden in HTTP/2 and default server timeouts that cut streams at 30 seconds.

A stream is a very slow request
According to a dev.to post by Jules Robineau, a Server-Sent Events endpoint is not a special case to an HTTP server: it is a request that never finishes. Every default guardrail in a server — write timeouts, context deadlines, idle timeouts — exists to kill requests that drag on. A legitimate stream looks exactly like the pathology those settings were written to terminate.
The author runs two SSE endpoints in production: a Go notification service on Kubernetes behind a reverse proxy, and an internal dashboard that refreshes without a page reload. Both broke in different places with the same visible symptom, a client reconnecting in an endless loop.
The Connection header that resets HTTP/2 streams
In the first incident, the endpoint answered 200 and the browser then reported net::ERR_HTTP2_PROTOCOL_ERROR. The cause was one line copied from an older SSE tutorial: setting Connection: keep-alive on the response.
Connection is a hop-by-hop header, meaning it applies to a single network hop rather than end to end, and RFC 9113 section 8.2.2 forbids such headers in HTTP/2. The browser speaks HTTP/2 to the ingress, the ingress re-emits the response, and the illegal header resets the stream immediately after the status line.
As the post points out, the header never did anything useful: HTTP/2 is persistent and multiplexed by design, and HTTP/1.1 already defaults to keep-alive. The recommended set is three headers only — Content-Type: text/event-stream, Cache-Control: no-cache, and X-Accel-Buffering: no for nginx — while never setting Connection, Keep-Alive, Transfer-Encoding or Upgrade.
Default timeouts kill the stream at 30 seconds
Days later a second failure appeared. A shared internal package that builds the author's HTTP servers configured ReadTimeout at 15 seconds, WriteTimeout at 30 seconds, IdleTimeout at 60 seconds, plus middleware that cancels the request context after 30 seconds.
Two of those values break SSE. WriteTimeout closes the connection while the handler is still writing, and the middleware cancels the context. The stream dies at exactly 30 seconds and surfaces as the same HTTP/2 error, so the symptom blames the protocol while the cause sits in configuration. The fix required both exempting the stream path from the timeout middleware and overriding WriteTimeout to zero for that route; either change on its own was not enough.
The other two timeouts are harmless in this setup, according to the post: ReadTimeout never fires because the client sends nothing after the initial request, and IdleTimeout is safe as long as writes happen more often than it expires — the author uses a heartbeat every 30 seconds against a 60-second limit.
On HTTP/1.1, a permanent stream freezes the page
A separate incident on the internal dashboard left buttons spinning because requests never returned. Browsers cap HTTP/1.1 at roughly six connections per origin, and an SSE stream permanently occupies one of them; a second tab can exhaust the pool. HTTP/2 removes the problem by multiplexing every request over a single connection.
The author's guard is to serve the stream only when the request arrived through the HTTPS front, checking the X-Forwarded-Proto header and returning 404 otherwise, so clients hitting the plain HTTP/1.1 origin never open a stream.
Go patched the same pattern in its own library
On 13 August 2026, per the post, the Go team shipped 1.26.6 and 1.25.13 with fixes for ten security issues. One of them, GO-2026-6089, also tracked as CVE-2026-56853, addressed ReadHeaderTimeout not being applied during the check for unencrypted HTTP/2 connections. A client could hold connections open without ever paying the timeout, a denial of service through resource exhaustion. The fix landed in go1.25.13, go1.26.6 and go1.27.0-rc.3, and Go 1.27 shipped on 19 August. The author's takeaway is the recurring pattern: the timeout existed, it simply did not cover the HTTP/2 path — the same failure mode as application-level configuration.
A checklist before shipping
The post closes with a pre-production list: no Connection, Keep-Alive, Transfer-Encoding or Upgrade headers in the handler; WriteTimeout disabled and timeout middleware bypassed on the stream path; a heartbeat firing more often than IdleTimeout; the stream served only behind an HTTP/2 front; no proxy buffering of the response; client reconnections counted; and a current Go version, standard library included.
Why it matters
Streaming responses are now routine — live dashboards, notifications, AI token streams — and Go's defaults are tuned for ordinary request-response APIs. The failure modes described here are silent or actively misleading: the browser reports an HTTP/2 protocol error when the real cause is an inherited tutorial header or a 30-second write timeout. The August 2026 patch shows the standard library itself is not immune to timeout blind spots, so teams on older Go releases have a concrete security reason to check their go.mod files alongside the operational ones.
- #go
- #http2
- #server-sent-events
- #timeouts
- #streaming