Server-Sent Events in ASP.NET Core Minimal APIs
You need to push real-time updates from server to browser. SignalR is the default answer, but sometimes you don’t need bidirectional communication. You just need the server to stream events to the client: a progress bar, a live feed, a notification stream.
Server-Sent Events (SSE) does exactly that. It’s a native browser API built on plain HTTP. No WebSocket upgrade, no SignalR hub, no client library.
The Basics
Set the content type to text/event-stream and write data: lines followed by double newlines:
app.MapGet("/events/progress", async (HttpContext context, CancellationToken ct) =>{ context.Response.ContentType = "text/event-stream"; context.Response.Headers.CacheControl = "no-cache";
string[] steps = ["Validating", "Processing", "Confirming"];
for (int i = 0; i < steps.Length; i++) { var json = JsonSerializer.Serialize(new { step = steps[i], percent = (i + 1) * 33 }); await context.Response.WriteAsync($"data: {json}\n\n", ct); await context.Response.Body.FlushAsync(ct); await Task.Delay(1500, ct); }});The CancellationToken fires when the client disconnects, so you stop doing work immediately.
Named Events
Add an event: line to categorize messages:
await context.Response.WriteAsync($"event: progress\ndata: {json}\n\n", ct);The client can then listen for specific event types.
The Client Side
The browser’s native EventSource API handles connection and auto-reconnection:
const source = new EventSource('/events/progress');
source.addEventListener('progress', (e) => { const data = JSON.parse(e.data); updateProgressBar(data.percent);});
source.onerror = () => console.log('Reconnecting...');Auto-reconnect is built into the spec. You get resilience for free.
Streaming with Channels
For fan-out scenarios like live dashboards, pair SSE with System.Threading.Channels:
app.MapGet("/events/orders", async (HttpContext ctx, OrderEventBus bus, CancellationToken ct) =>{ ctx.Response.ContentType = "text/event-stream"; ctx.Response.Headers.CacheControl = "no-cache";
await foreach (var evt in bus.Subscribe(ct)) { var json = JsonSerializer.Serialize(evt); await ctx.Response.WriteAsync($"data: {json}\n\n", ct); await ctx.Response.Body.FlushAsync(ct); }});POST events into the channel from elsewhere, and every connected SSE client receives them in real time.
SSE vs. SignalR
| SSE | SignalR | |
|---|---|---|
| Direction | Server to client only | Bidirectional |
| Transport | Plain HTTP | WebSocket (with fallbacks) |
| Client | Native EventSource | Requires SignalR library |
| Auto-reconnect | Built into the spec | Built into the client |
| Binary data | Text only | Text and binary |
Choose SSE when you only need server-to-client streaming and want the simplest possible setup. Choose SignalR when you need bidirectional messaging, binary data, or group management.
Key Takeaway
SSE is the lightest way to push real-time updates from ASP.NET Core to a browser. Set text/event-stream, write data: lines, flush, and the browser handles the rest, including automatic reconnection.