.NET 10 Server-Sent Events
Reading Time: 6 minutes
The Problem
Not every application needs full two-way real-time communication.
Sometimes the server just needs to push updates out to the browser:
- progress notifications
- build or deployment status
- live dashboard values
- audit or activity feeds
- background job updates
For those cases, WebSockets can be more infrastructure and protocol surface than the problem actually needs.
Before .NET 10, Server-Sent Events usually meant dropping down to manual response handling:
- setting the
text/event-streamcontent type yourself - writing raw
data:lines to the response body - remembering to flush after each message
- handling disconnects and cancellation explicitly
That worked, but it was low-level and easy to get wrong. The protocol is simple. The implementation details were what made it awkward.
The Solution
.NET 10 adds built-in support for Server-Sent Events in ASP.NET Core, which makes streaming one-way updates much cleaner from minimal APIs.
That matters because SSE is often the right tool when:
- the server only needs to push data to the client
- the client is usually a browser
- you want a simpler model than WebSockets
- automatic reconnection is useful
- plain HTTP infrastructure is preferable
The result is a much nicer developer experience for a very common class of real-time features.
Instead of treating SSE like a special-case manual stream, .NET 10 lets you expose it more directly as an endpoint result.
What Server-Sent Events Actually Are
Server-Sent Events use a long-lived HTTP connection where the server keeps sending messages over time.
The browser consumes those messages through the EventSource API.
That makes SSE a good fit for one-directional streams such as:
- live counters
- queue depth indicators
- notifications
- telemetry feeds
- workflow progress
It is not the right choice when the client also needs to send frequent real-time messages back to the server. That is where WebSockets still make more sense.
The key distinction is simple:
- SSE: server to client
- WebSockets: two-way communication
Why .NET 10 Improves the Story
The main improvement in .NET 10 is that SSE becomes part of the framework’s normal response model instead of feeling like custom stream plumbing.
That gives you a cleaner shape for endpoints that produce a sequence of values over time.
The practical win is not that SSE is a new protocol. It is not. The win is that the framework now makes the protocol easier to expose correctly.
This lowers the friction for building features like:
- operation status streams
- real-time admin dashboards
- deployment progress UIs
- activity timelines
- lightweight monitoring views
A Simple .NET 10 SSE Endpoint
With minimal APIs, the model is straightforward: return a server-sent event stream backed by an async sequence.
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/time-stream", (CancellationToken cancellationToken) =>
TypedResults.ServerSentEvents(GetTimeUpdates(cancellationToken)));
app.Run();
static async IAsyncEnumerable<string> GetTimeUpdates(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return $"UTC time: {DateTime.UtcNow:O}";
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
This is the core idea:
- the endpoint stays open
- the server yields messages over time
- the client receives them as an event stream
- cancellation stops the stream when the client disconnects or the request ends
That is a much cleaner shape than manually writing event frames into the response body.
A More Realistic Example
SSE becomes more useful when the data has structure.
For example, a background job progress stream:
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/jobs/{jobId}/progress", (string jobId, CancellationToken cancellationToken) =>
TypedResults.ServerSentEvents(GetProgressUpdates(jobId, cancellationToken)));
app.Run();
static async IAsyncEnumerable<JobProgressUpdate> GetProgressUpdates(
string jobId,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
for (var percent = 0; percent <= 100; percent += 10)
{
yield return new JobProgressUpdate(jobId, percent, $"Processing {percent}%");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
public sealed record JobProgressUpdate(string JobId, int PercentComplete, string Message);
This pattern is a good fit when the browser wants immediate updates without polling every few seconds.
Why SSE Is Often Better Than Polling
Polling usually starts as the simplest thing that could work.
The browser calls an endpoint every few seconds and asks whether anything changed.
That creates familiar problems:
- too many wasted requests when nothing changes
- slower user experience because updates wait for the next poll interval
- extra pressure on the API for no real value
SSE avoids that by keeping one connection open and pushing new data only when there is something to send.
For status streams and dashboards, that is usually a better operational model than repeated polling.
Why SSE Is Often Better Than WebSockets
WebSockets are powerful, but they also come with additional complexity:
- bidirectional protocol handling
- connection lifecycle management beyond simple HTTP request/response thinking
- more moving parts than you need for server-only updates
If your use case is just “the server tells the browser when something changes,” SSE is often the more pragmatic option.
That is why the built-in .NET 10 support matters. It gives ASP.NET Core a clean first-class answer for lightweight server push.
Operational Considerations
SSE is simple, but it still has runtime behavior you should think about.
Keep cancellation wired correctly
Always respect the request cancellation token. When the client disconnects, your stream should stop producing values.
Do not treat it like a firehose by default
SSE is excellent for incremental updates. It is not an excuse to stream enormous message volumes to every connected client without limits.
Think about proxies and timeouts
Because SSE keeps HTTP requests open for longer, reverse proxies, platform timeouts, and buffering behavior still matter.
Design event payloads carefully
Small, meaningful payloads are easier to consume than giant objects repeated every second.
When SSE Is a Strong Fit
.NET 10 SSE support is especially useful for:
- admin dashboards
- build or deployment progress screens
- workflow or import job tracking
- notification feeds
- monitoring pages with lightweight live updates
If the client only needs to receive updates, SSE is usually worth considering before jumping to WebSockets.
Practical Recommendations
A strong default approach is:
- use SSE when the communication is one-way
- expose streams from minimal APIs with async sequences
- honor cancellation tokens so abandoned streams stop cleanly
- keep payloads small and meaningful
- prefer SSE over polling when update latency matters
- prefer SSE over WebSockets when the client does not need to send real-time messages back
Summary
.NET 10 makes Server-Sent Events much more approachable in ASP.NET Core.
The protocol itself has always been useful for one-way real-time updates, but the developer experience used to be more manual than it should have been. Built-in SSE support changes that.
For the right category of problem, that is a real improvement.
You can now model lightweight server push in a way that feels natural inside minimal APIs:
- define an async sequence
- return it as an SSE stream
- let the client consume updates as they arrive
That is simpler than polling, lighter than WebSockets for one-way scenarios, and a strong fit for many real-world .NET applications.
References
- Server-sent events on MDN: https://developer.mozilla.org/docs/Web/API/Server-sent_events