SSE transport batches multiple events under high throughput, breaking SSE framing
#3,960 opened on Dec 9, 2025
Repository metrics
- Stars
- (10,737 stars)
- PR merge metrics
- (PR metrics pending)
Description
Summary
When using the SSE transport with high-throughput subscriptions, multiple SSE events can be concatenated into a single data: line, breaking SSE protocol framing and causing downstream parsers to fail with "Invalid message" errors.
Environment
- gqlgen version: v0.17.84
- Go version: 1.21+
- OS: Linux/macOS (observed in Kubernetes environment)
- frontend subscribe -> Wundergraph router -> subgraph subscription resolver <->channel of events
Expected Behavior
Each subscription event should be sent as a separate SSE frame:
event: next
data: {"data":{"mySubscription":{"field":"value1"}}}
event: next
data: {"data":{"mySubscription":{"field":"value2"}}}
Actual Behavior
Under high throughput, multiple events are concatenated on the same line and we receive on the event stream:
event: next
data: {"errors":[{"message":"Invalid message received"}],"data":null,"extensions":...
This causes SSE parsers (including WunderGraph Cosmo router) to fail with "Invalid message received" errors because they receive malformed JSON.
Root Cause Analysis
In transport/sse.go, the write loop (lines 116-126) calls writeJsonWithSSE() followed by c.flush():
for {
response := responses(ctx)
if response == nil {
break
}
writeJsonWithSSE(w, response)
c.flush()
// ...
}
The issue is that http.Flusher.Flush() only guarantees data is pushed to the kernel's TCP buffer, not that it's actually sent over the network. When events arrive faster than the network can transmit them, multiple writeJsonWithSSE calls can write to the http.ResponseWriter's internal buffer before the data is actually transmitted, resulting in concatenated SSE frames.
Workaround
Adding a small delay (1ms) after each flush ensures the TCP stack has time to send each frame separately:
writeJsonWithSSE(w, response)
c.flush()
time.Sleep(1 * time.Millisecond) // Workaround for SSE frame batching
- Trying
runtime.Sched()instead of sleep didn't work
Reproduction
- Create a subscription that emits events rapidly (e.g., from a buffered channel with 100+ events ready)
- Subscribe via SSE transport
- Observe that events are batched/concatenated in the SSE stream
Examples
Here's a network view on the developer console:
Possible Solutions
- Add a configurable delay option to the SSE transport struct (e.g., MinEventInterval time.Duration)
- Document the limitation for high-throughput scenarios
Related
This may be related to how Go's net/http handles chunked transfer encoding with rapid writes. The http.ResponseWriter uses a bufio.Writer internally which can buffer multiple writes before they're sent as a single HTTP chunk.