C# Server-Sent Events
last modified October 17, 2026
In this article we show how to send and receive Server-Sent Events in C#.
Server-Sent Events (SSE) is a simple standard for pushing a stream
of events from a server to a client over a single HTTP response. The response
has the text/event-stream content type and stays open; the server
writes events as they happen. Unlike WebSockets, the communication is one-way,
from the server to the client, and it is plain HTTP, so it works through
proxies and needs no special protocol. Typical uses are live notifications,
progress updates, dashboards, and streaming the output of AI models.
Each event is a block of field: value lines terminated by an
empty line. The most common fields are data, event
(the event type), and id.
event: price
data: {"symbol":"ZETC","price":97.25}
id: 1
In .NET 10, ASP.NET Core can produce event streams with
TypedResults.ServerSentEvents, and the
System.Net.ServerSentEvents namespace, which is part of the base
library, provides the SseParser class for reading them.
C# SSE server
We create an ASP.NET Core application that streams stock price updates.
$ dotnet new web -o Server
The web template creates an empty ASP.NET Core project.
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/prices", (CancellationToken token) =>
TypedResults.ServerSentEvents(GetPricesAsync(token)));
app.Run("http://localhost:5050");
static async IAsyncEnumerable<SseItem<StockPrice>> GetPricesAsync(
[EnumeratorCancellation] CancellationToken token)
{
decimal price = 100m;
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000, token);
price += Random.Shared.Next(-300, 301) / 100m;
var update = new StockPrice("ZETC", price, DateTime.Now);
yield return new SseItem<StockPrice>(update, "price") { EventId = i.ToString() };
}
yield return new SseItem<StockPrice>(new StockPrice("ZETC", price, DateTime.Now), "done");
}
record StockPrice(string Symbol, decimal Price, DateTime Time);
The /prices endpoint sends five price updates, one per second,
followed by a done event.
app.MapGet("/prices", (CancellationToken token) =>
TypedResults.ServerSentEvents(GetPricesAsync(token)));
The ServerSentEvents method takes an asynchronous sequence of
items and writes each item as an event while the sequence produces it. The
cancellation token is signalled when the client disconnects.
static async IAsyncEnumerable<SseItem<StockPrice>> GetPricesAsync(
[EnumeratorCancellation] CancellationToken token)
The events are produced by an async iterator. The
[EnumeratorCancellation] attribute connects the token with the
cancellation of the enumeration.
yield return new SseItem<StockPrice>(update, "price") { EventId = i.ToString() };
An SseItem holds the data together with the event type and an
optional event id. Objects are serialized to JSON. There are also overloads
that take a sequence of plain strings or objects with one event type for all
items.
We run the server and look at the raw event stream with curl. The
-N option turns off the buffering of the output, so we see the
events as they arrive.
$ curl -N localhost:5050/prices
event: price
data: {"symbol":"ZETC","price":97.25,"time":"2026-10-17T19:21:14.6246006+02:00"}
id: 1
event: price
data: {"symbol":"ZETC","price":99.66,"time":"2026-10-17T19:21:15.6666353+02:00"}
id: 2
...
event: done
data: {"symbol":"ZETC","price":102.95,"time":"2026-10-17T19:21:18.676339+02:00"}
C# SSE client
In browsers, event streams are read with the JavaScript
EventSource API. In C#, we read them with HttpClient
and the SseParser class.
$ dotnet new console -o Client
We create a console project for the client.
using System.Net.ServerSentEvents;
using System.Text.Json;
using var client = new HttpClient();
using Stream stream = await client.GetStreamAsync("http://localhost:5050/prices");
SseParser<string> parser = SseParser.Create(stream);
await foreach (SseItem<string> item in parser.EnumerateAsync())
{
var update = JsonSerializer.Deserialize<StockPrice>(item.Data, JsonSerializerOptions.Web);
if (item.EventType == "done")
{
Console.WriteLine($"Stream finished, last price: {update?.Price}");
break;
}
Console.WriteLine($"#{item.EventId} {update?.Symbol} {update?.Price:F2} at {update?.Time:HH:mm:ss}");
}
record StockPrice(string Symbol, decimal Price, DateTime Time);
The client prints the price updates until the done event arrives.
using Stream stream = await client.GetStreamAsync("http://localhost:5050/prices");
The GetStreamAsync method returns as soon as the response headers
are received; the body is read from the stream as the server writes it.
SseParser<string> parser = SseParser.Create(stream); await foreach (SseItem<string> item in parser.EnumerateAsync())
The parser splits the stream into events. The EnumerateAsync
method returns an asynchronous sequence, which we consume with
await foreach. Each item has the Data,
EventType, and EventId properties. Events without the
event field have the message type.
var update = JsonSerializer.Deserialize<StockPrice>(item.Data, JsonSerializerOptions.Web);
The data is JSON with camel-case property names, so we deserialize it with the
web defaults. SseParser.Create also has an overload that takes a
parser function, which turns the data into a typed object directly.
if (item.EventType == "done")
{
Console.WriteLine($"Stream finished, last price: {update?.Price}");
break;
}
We react to different event types. Leaving the loop disposes the enumerator, and disposing the stream closes the connection.
$ dotnet run #1 ZETC 97.08 at 19:21:20 #2 ZETC 99.55 at 19:21:21 #3 ZETC 99.13 at 19:21:22 #4 ZETC 99.62 at 19:21:23 #5 ZETC 101.10 at 19:21:24 Stream finished, last price: 101.10
The updates appear one per second, as the server sends them.
Source
Server-sent events - HTML Living Standard
SseParser class - language reference
In this article we have sent and received Server-Sent Events in C#.
Author
List all C# tutorials.