Streaming JSON vs SSE in .NET 10: A Performance Deep Dive
Stop blindly trusting the "JSON responses always buffer" myth. I recently challenged a code review comment that insisted IAsyncEnumerable in a minimal API requires SignalR for real-time updates, and the results were eye-opening.
The Streaming Reality
I set up a test with a fake job yielding steps every 400ms to see if the server actually buffers.
app.MapGet("/steps/json", (CancellationToken ct) => Produce(padding: 0, ct));
async IAsyncEnumerable Produce(int padding, [EnumeratorCancellation] CancellationToken ct = default)
{
for (var i = 1; i <= 8; i++)
{
await Task.Delay(400, ct);
yield return new Step(i, $"step {i}/8", "");
}
}
Using DeserializeAsyncEnumerable on the client side, the data arrived exactly as it was yielded—no buffering. The "delay" people experience is usually a browser issue; fetch().json() waits for the entire body to close before resolving. This creates a false impression that the server is holding the data back.
Transitioning to Server-Sent Events (SSE)
While JSON streaming works for service-to-service calls, .NET 10 introduces a much cleaner way to handle browser-side streaming via TypedResults.ServerSentEvents.
app.MapGet("/steps/sse", (CancellationToken ct) =>
TypedResults.ServerSentEvents(ProduceSse(ct)));
async IAsyncEnumerable<SseItem> ProduceSse([EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var step in Produce(padding: 0, ct))
yield return new SseItem(step, eventType: "step") { EventId = step.Number.ToString() };
}
On the frontend, this replaces complex SignalR hubs with a native EventSource implementation:
const source = new EventSource("/steps/sse");
source.addEventListener("step", e => render(JSON.parse(e.data)));
Practical Trade-offs
After running the numbers and testing the deployment, here is the breakdown:
- Overhead: SSE adds framing (about 60% increase for tiny payloads), but it's negligible for real-world data.
- Connectivity:
EventSourcehandles reconnections andLast-Event-Idautomatically. - Constraints: In HTTP/1.1, browsers limit connections per origin. Use HTTP/2 to avoid blocking other requests.
- When to still use SignalR: For bidirectional communication or massive fan-out scenarios requiring a backplane.
If you want to see the full implementation, the source is available here:https://github.com/ssukhpinder/dev-to-code-samples/tree/main/004-json-streaming-vs-sse
All Replies (3)
IAsyncEnumerable is a lifesaver for log tails, but proxy buffering is a nightmare. How are you handling that?
Tried this for live feeds last month and the speed was wild. Anyone else see those gains?
Worried about the GC melting down under high load. Has anyone benchmarked this at scale?