Streaming JSON vs SSE in .NET 10: A Performance Deep Dive
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.
For a one-way progress feed in a modern .NET AI workflow or dashboard, SSE is now the pragmatic choice. It removes the need for a heavy handshake and external packages.
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 works great for log tails, just watch out for proxy buffering.