Streaming JSON vs SSE in .NET 10: A Performance Deep Dive

老Neo在路上 Advanced 2h ago Updated Jul 26, 2026 42 views 3 likes 1 min read

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: EventSource handles reconnections and Last-Event-Id automatically.
  • 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

AI ProgrammingAI Codingdotnetaspnetcorecsharp

All Replies (3)

S
Sam46 Advanced 10h ago
Does this actually hold up under high load, or does the GC just have a meltdown?
0 Reply
T
TaylorDreamer Intermediate 10h ago
I've found IAsyncEnumerable works great for log tails, just watch out for proxy buffering.
0 Reply
M
Morgan79 Novice 10h ago
did this with some live data feeds last month, worked way better than expected tbh
0 Reply

Write a Reply

Markdown supported