Skip to main content
SEMastery
ASP.NETbeginner

Real-Time Server-Sent Events in ASP.NET Core and .NET 10

Learn Server-Sent Events (SSE) in ASP.NET Core and .NET 10 with the new TypedResults.ServerSentEvents API, explained simply for beginners.

12 min readUpdated April 6, 2026

Picture a railway station with a big announcement board. You sit on a bench. You do not walk to the counter every minute asking "Has my train arrived? Has my train arrived?" Instead, the station keeps announcing updates on its own. "Train 12345 is now on platform 3." You just listen. The station talks, you receive.

That is exactly what Server-Sent Events (SSE) do for a website. The server keeps one connection open and keeps announcing new things. Your browser just listens. You never press refresh.

In this guide we will learn what SSE is, when to use it, and how the brand-new support in ASP.NET Core and .NET 10 makes it very easy. We will keep the words small and the steps friendly.

What problem does SSE solve?

Normally the web works like a question and answer. Your browser asks the server something, the server replies, and the connection closes. Like sending a letter and waiting for a reply letter.

But what about a live cricket score? Or a stock price? Or a "your food is being cooked" status? With the old way, the browser has to keep asking again and again. This is called polling, and it wastes time and data.

SSE flips this. The browser opens one connection and says "keep me posted." Then the server pushes new messages down that same pipe whenever it has something. The browser listens with an open ear.

Polling keeps asking; SSE just listens once

SSE versus WebSockets versus SignalR

People often mix these up. Here is the simple difference.

FeaturePollingSSEWebSockets
DirectionTwo-way, but slowServer to client onlyBoth ways
ConnectionNew each timeOne stays openOne stays open
Built into browserYes (plain fetch)Yes (EventSource)Yes (WebSocket)
Auto-reconnectYou write itBrowser does itYou write it
Best forRare checksLive feeds, alertsChat, games

The big idea: if updates flow only from server to browser, SSE is the lightest and simplest choice. You do not need a heavy library. You do not need a fancy handshake. It is just normal HTTP with a special content type called text/event-stream.

SignalR (covered in another article) is a richer tool that can use WebSockets, SSE, or polling under the hood. SSE on its own is smaller and easier when you only push one way.

How an SSE message looks

An SSE stream is just plain text sent over time. Each message is a few lines. A simple message looks like this:

event: priceUpdate
id: 42
data: {"symbol":"INFY","price":1530}
 

The blank line at the end tells the browser "this message is done." The event line is a label, the id line helps with reconnection, and the data line carries your actual payload. Before .NET 10, you had to write this text format by hand. Now the framework writes it for you.

Anatomy of one SSE message

event
id
data
blank line

Steps

1

event

A label like priceUpdate

2

id

Used to resume after a drop

3

data

Your real content

4

blank line

Marks the end of the message

The four parts the browser understands

The new way in .NET 10

Starting in .NET 10, ASP.NET Core added a result type built for this job. You return TypedResults.ServerSentEvents(...) and hand it an IAsyncEnumerable<T>. The base libraries also added a new System.Net.ServerSentEvents namespace and a type called SseItem<T>.

When you return this result, ASP.NET Core notices it. It quietly adds a filter that keeps the connection open instead of closing it. As your async sequence yields items one by one, the framework formats each one into the SSE text format and streams it to the browser. You only write the data; the plumbing is done for you.

SseItem<T> has four properties, and only one is required:

PropertyRequired?What it does
DataYesThe payload you want to send
EventTypeNoA label so the client can tell events apart
EventIdNoAn id sent back as Last-Event-ID on reconnect
ReconnectionIntervalNoHow long the browser waits before retrying

A first tiny example

Let us build an endpoint that streams a fake heart rate every second. We use an async method that yields numbers forever, and we wrap each one with TypedResults.ServerSentEvents.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
app.MapGet("/heart-rate", (CancellationToken ct) =>
{
    async IAsyncEnumerable<int> GetHeartRate(
        [EnumeratorCancellation] CancellationToken token)
    {
        var random = new Random();
        while (!token.IsCancellationRequested)
        {
            yield return random.Next(60, 100);
            await Task.Delay(1000, token);
        }
    }
 
    return TypedResults.ServerSentEvents(GetHeartRate(ct), eventType: "heartRate");
});
 
app.Run();

A few things to notice. The CancellationToken is important. When the browser closes the tab, the token is cancelled, the loop stops, and we do not leak a runaway task. The eventType of "heartRate" becomes the label on every message, so the browser can listen for exactly that name.

What happens when a client connects to the SSE endpoint

Reading the stream in the browser

The browser side is short and sweet. The built-in EventSource object opens the connection and fires an event each time a message arrives.

const source = new EventSource("/heart-rate");
 
source.addEventListener("heartRate", (event) => {
  console.log("New heart rate:", event.data);
});
 
source.onerror = () => {
  console.log("Connection dropped, browser will retry...");
};

Notice we listen for "heartRate" because that was our eventType. If you do not set an event type, you can listen with the generic source.onmessage instead. The best part: if the network blinks and the connection drops, the browser reconnects on its own. You did not write a single line for that.

Sending rich objects, not just numbers

Real apps send objects, not plain integers. You can yield any type and let JSON serialization handle it. Here is a stream of order updates.

public record OrderUpdate(int OrderId, string Status, DateTime At);
 
app.MapGet("/orders/stream", (CancellationToken ct) =>
{
    async IAsyncEnumerable<SseItem<OrderUpdate>> Stream(
        [EnumeratorCancellation] CancellationToken token)
    {
        var stages = new[] { "Placed", "Cooking", "Out for delivery", "Delivered" };
        var id = 0;
        foreach (var stage in stages)
        {
            if (token.IsCancellationRequested) yield break;
 
            var update = new OrderUpdate(101, stage, DateTime.UtcNow);
            yield return new SseItem<OrderUpdate>(update, eventType: "orderUpdate")
            {
                EventId = (++id).ToString()
            };
            await Task.Delay(2000, token);
        }
    }
 
    return TypedResults.ServerSentEvents(Stream(ct));
});

Here we build each SseItem<OrderUpdate> ourselves so we can set an EventId. That id matters for reconnection, which we cover next.

Reconnection and resuming where you left off

Say the connection drops at message 3. When the browser reconnects, it sends a header called Last-Event-ID with the id of the last message it saw. Your server can read that header and decide to replay only the messages the client missed. This is how you avoid sending duplicates or losing updates.

Resuming after a dropped connection

Drop at id 3
Browser reconnects
Sends Last-Event-ID: 3
Server replays from 4

Steps

1

Drop at id 3

Network blinks

2

Browser reconnects

Automatic, no code

3

Sends Last-Event-ID: 3

In the request header

4

Server replays from 4

Only the missed items

How Last-Event-ID lets the client catch up

To read that header on the server, you simply accept it as a parameter or read it from the request headers, then start your stream from the next id. Keep a small buffer or log of recent events so you have something to replay.

When should you pick SSE?

SSE shines when data flows one way, from server to client, and you want something simple. Good fits include live dashboards, notification bells, progress bars for long jobs, log tailing, and streaming AI text responses token by token. In fact, streaming chat replies from a language model is a very popular use of SSE today.

SSE is not the right pick when the client also needs to send a constant stream back, like in a multiplayer game or a two-way chat where typing indicators fly both ways. For that, reach for WebSockets or SignalR.

One more practical note. A single browser is limited to about six open HTTP connections per domain over HTTP/1.1, and SSE uses one of them while open. If you serve over HTTP/2 this limit largely goes away, because many streams share one connection. So serving SSE over HTTP/2 is a smart move for busy pages.

Choosing between SSE and WebSockets

A real example: live notifications

Let us think about a real screen you have seen many times. The little bell icon at the top of a website that shows new notifications. When a friend likes your photo or a seller ships your parcel, a small red dot appears. You did not refresh the page. The server pushed that news to you. This is a textbook job for SSE.

Imagine the backend. Somewhere in your system, an event happens. Maybe a payment was confirmed, or a comment was added. You drop that event into a channel or a queue inside your app. Your SSE endpoint reads from that channel and yields each new notification to the connected browser. The browser receives it and animates the bell. Because the connection stays open, the delay between the event and the bell is tiny, often a fraction of a second.

This pattern scales nicely for read-heavy screens. Many users can each hold one open stream, and the server only does work when there is something real to send. Compare that to polling, where a thousand idle users would hammer your server every few seconds asking "anything new?" even when nothing changed. SSE stays quiet until there is news, which saves a lot of wasted effort on both sides.

A small but important design tip: keep each notification message small. SSE is great for short, frequent updates. If you need to send a large blob of data, send only an id over SSE, then let the browser fetch the full details with a normal request. This keeps your stream snappy and your memory low.

Testing your SSE endpoint quickly

You do not even need a browser to see SSE working. The curl command line tool can connect and print the raw stream. This is a fast way to check that your endpoint streams correctly before you write any frontend code.

When you run a curl request against the endpoint, you will see the messages arrive line by line, each ending with a blank line, exactly in the format we looked at earlier. If you see all the data appear at once at the very end instead of trickling in, that is a clue that something in your pipeline is buffering, and you should hunt down what is holding the data back.

For automated tests, you can use a typed HTTP client in .NET and read the response stream as it arrives. Because the new namespace ships a parser too, you can read the incoming events back into SseItem<T> objects in your test code and assert on them. This makes it easy to prove your endpoint sends the right events in the right order without spinning up a real browser.

Common mistakes to avoid

A few traps catch beginners, so let us name them clearly.

First, always honor the CancellationToken. If you ignore it, your server keeps looping for clients who already left, and your CPU and memory slowly fill up. The token is your "the listener walked away" signal.

Second, do not buffer the whole stream. The point of SSE is to send each item as it is ready. If something in your pipeline collects everything first, the client sees nothing until the end, which defeats the purpose.

Third, mind proxies and timeouts. Some reverse proxies or load balancers cut idle connections after a while. Sending a tiny keep-alive comment line now and then keeps the pipe warm. A comment line starts with a colon and is ignored by the browser.

Fourth, remember SSE only carries text. If you need to send binary data, encode it (for example as base64) inside the data field.

Quick recap

  • SSE lets the server push a stream of updates to the browser over one open HTTP connection, like a station announcement board.
  • It is one-way (server to client), lightweight, and uses the text/event-stream content type.
  • The browser's built-in EventSource reads the stream and reconnects automatically for free.
  • .NET 10 added first-class support: return TypedResults.ServerSentEvents with an IAsyncEnumerable<T> or IAsyncEnumerable<SseItem<T>>.
  • SseItem<T> carries Data, plus optional EventType, EventId, and ReconnectionInterval.
  • Use EventId and the Last-Event-ID header to resume after a dropped connection.
  • Always respect the CancellationToken, stream items one at a time, and prefer HTTP/2 for busy pages.
  • Choose SSE for one-way feeds; choose WebSockets or SignalR when both sides need to talk.

References and further reading

Related Posts