ZetCode

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.

Server/Program.cs
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.

Client/Program.cs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.