Server-Sent Events (SSE) is a standard protocol that enables a server to push real-time updates to clients over a single, long-lived HTTP connection. Unlike WebSockets, SSE is unidirectional — data flows only from the server to the client — making it a lightweight and straightforward choice for scenarios where the client simply needs to receive a continuous stream of updates without sending data back.
This project is a demo API built with ASP.NET Core Minimal APIs on .NET 10. It exposes a single /SSE endpoint that streams randomly generated notification messages (powered by the Bogus library) using the text/event-stream content type. The goal is to provide a clear, minimal example of how to implement server-sent events in a .NET application, useful for learning, prototyping, and testing SSE-based integrations.
When building applications that need to push data from the server to the client, there are three main approaches to consider:
| Approach | How It Works | Direction | Trade-offs |
|---|---|---|---|
| HTTP Polling | The client sends repeated requests at a fixed interval to check for new data. | Client → Server | Simple to implement, but wasteful — most requests return empty responses, adding unnecessary network overhead and latency. |
| WebSocket / SignalR | A full-duplex connection is established over a single TCP socket, allowing both the client and server to send messages at any time. In .NET, SignalR abstracts WebSocket management with fallback transports. | Bidirectional | Powerful and flexible, but adds complexity in connection management, protocol negotiation, and infrastructure (load balancers, proxies). Overkill when the client never needs to send data back. |
| Server-Sent Events (SSE) | The client opens a standard HTTP GET request and the server keeps the connection alive, writing data: frames as new events become available. Built on the text/event-stream content type. |
Server → Client | Lightweight, uses plain HTTP, and includes automatic reconnection via the browser's EventSource API. Limited to text-based, unidirectional streaming. |
For scenarios where the server is the sole producer of data and the client simply consumes updates, SSE is the most practical choice:
- Lower overhead than polling — SSE maintains a single persistent connection instead of opening and closing a new HTTP request on every interval. This eliminates redundant request/response cycles, reduces bandwidth consumption, and delivers updates with near-zero latency.
- Simpler than WebSocket — SSE works over standard HTTP, so it passes through firewalls, proxies, and load balancers without special configuration. There is no protocol upgrade handshake, no frame masking, and no need to manage a bidirectional channel when only the server needs to send data.
- Native browser support — The
EventSourceAPI provides built-in connection management with automatic reconnection and last-event-ID tracking, requiring minimal client-side code. - Sufficient for unidirectional use cases — Dashboards, live feeds, notification streams, and progress indicators all follow the same pattern: the server pushes, the client listens. SSE fits this pattern exactly, without the added complexity of a bidirectional protocol.
This project uses SSE because the demo scenario — streaming server-generated notifications to a client — is purely unidirectional. WebSocket and SignalR would introduce unnecessary complexity, while HTTP polling would waste resources on empty responses.
- Real-time streaming — The
/SSEendpoint pushes server-generated events to clients over a persistent HTTP connection using thetext/event-streamcontent type. - Mock data generation — Notification messages with random titles and content are generated on the fly using the Bogus library, simulating a live data feed without requiring a database or external service.
- Swagger UI documentation — Interactive API documentation is available via Swashbuckle.AspNetCore, accessible at
/swaggerin Development mode for easy endpoint exploration and testing.
The project follows a single-project Minimal API architecture. All routing is defined in Program.cs, which coordinates data generation and SSE streaming.
graph LR
Client["Client"]
SSE["/SSE Endpoint<br/>(Program.cs)"]
Service["MoqDataRetrieverService"]
Model["NotificationMessage"]
Swagger["Swagger UI<br/>(Swashbuckle)"]
Client -->|GET /SSE| SSE
SSE --> Service
Service -->|generates| Model
Model -->|serialized to JSON| SSE
SSE -->|text/event-stream| Client
SSE -.->|/swagger| Swagger
Data flow: The client opens a persistent connection to the /SSE endpoint. Program.cs calls MoqDataRetrieverService, which uses the Bogus library to generate random NotificationMessage objects. Each message is serialized to JSON via System.Text.Json and streamed back to the client as a data: frame over the text/event-stream connection. Swagger UI is available at /swagger in Development mode for interactive API exploration.
The following sequence diagram illustrates the lifecycle of a single SSE connection, from the initial client request through the streaming loop to disconnection.
sequenceDiagram
participant Client
participant SSE as /SSE Endpoint
participant Service as MoqDataRetrieverService
participant Json as System.Text.Json
Client->>SSE: GET /SSE
SSE->>SSE: Set Content-Type: text/event-stream
loop While CancellationToken is not cancelled
SSE->>Service: GetNewMessages()
Service-->>SSE: List of NotificationMessage
loop For each message
SSE->>Json: Serialize message to JSON
Json-->>SSE: JSON string
SSE->>Client: data: {json}\n\n
SSE->>SSE: Flush response body
end
end
Client->>SSE: Disconnect (CancellationToken cancelled)
SSE-->>Client: Connection closed
How it works: When a client sends a GET /SSE request, the server sets the Content-Type header to text/event-stream and enters a continuous loop. On each iteration, MoqDataRetrieverService.GetNewMessages() generates a batch of random NotificationMessage objects. Each message is serialized to JSON via System.Text.Json, written to the response as a data: frame followed by two newlines (\n\n), and flushed to the client immediately. The loop continues until the client disconnects, which triggers the CancellationToken and ends the stream gracefully.
Before running the project, make sure you have the following installed:
- .NET 10 SDK — required to build and run the application
- A compatible IDE or editor, such as Visual Studio 2022 (17.x+), Visual Studio Code with the C# Dev Kit extension, or JetBrains Rider
- Clone the repository
git clone https://github.com/<your-username>/ServerSideEvents.git
cd ServerSideEvents- Restore dependencies
dotnet restore- Build the solution
dotnet build- Run the application
Using the default http profile (http://localhost:5059):
dotnet run --project ServerSideEventsOr using the https profile (https://localhost:7095):
dotnet run --project ServerSideEvents --launch-profile https- Access Swagger UI
Once the application is running in Development mode, open your browser and navigate to:
- http profile:
http://localhost:5059/swagger - https profile:
https://localhost:7095/swagger
- Connect to the SSE endpoint
To start receiving real-time events, open the SSE stream in your browser or with a tool like curl:
- http profile:
http://localhost:5059/SSE - https profile:
https://localhost:7095/SSE
curl http://localhost:5059/SSEServerSideEvents.sln # Solution file (single project)
ServerSideEvents/ # Main web API project
├── Program.cs # Application entry point, DI setup, and all route definitions
├── NotificationMessage.cs # Data model for SSE notification payloads
├── MoqDataRetrieverService.cs # Singleton service that generates fake notification data via Bogus
├── ServerSideEvents.csproj # Project file (.NET 10 Web SDK)
├── ServerSideEvents.http # HTTP request file for quick endpoint testing
├── Properties/
│ └── launchSettings.json # Dev server profiles (http, https, IIS Express)
├── appsettings.json # Base application configuration
└── appsettings.Development.json # Development-specific overrides
assets/ # Static assets (screenshots for README)
└── postmanTest.png # Postman testing screenshot
README.md # Project documentation
You can verify the SSE endpoint using a terminal with curl or a GUI tool like Postman.
Open a terminal and run the following command to connect to the SSE stream:
curl -N http://localhost:5059/SSEThe -N flag disables output buffering, so events are displayed as soon as the server sends them. You should see a continuous stream of data: frames printed to the terminal. Press Ctrl+C to disconnect.
- Open Postman
- Create a new GET request
- Enter the SSE endpoint URL:
http://localhost:5059/SSE - Click Send
- Observe real-time updates appearing in the response body as the server pushes new events
Postman will keep the connection open and display each incoming event as it arrives.
Postman receiving real-time notification events from the /SSE endpoint.
Each event in the SSE stream is delivered as a data: frame containing a JSON-serialized NotificationMessage object:
data: {"Title":"Some random title","Message":"Some random message"}
The NotificationMessage model has two string properties:
| Field | Type | Description |
|---|---|---|
Title |
string |
A randomly generated notification title |
Message |
string |
A randomly generated notification message |
Messages are generated by the MoqDataRetrieverService using the Bogus library and serialized with System.Text.Json.
| Technology | Version | Purpose |
|---|---|---|
| .NET | 10.0 | Runtime and SDK for building and running the application |
| ASP.NET Core Minimal APIs | 10.0 | Lightweight web framework for defining HTTP endpoints without controllers |
| Bogus | 35.6.5 | Fake data generation library used to create random notification messages |
| Microsoft.AspNetCore.OpenApi | 10.0.5 | OpenAPI metadata support for Minimal API endpoints |
| Swashbuckle.AspNetCore | 10.1.7 | Swagger UI generation and interactive API documentation at /swagger |
| System.Text.Json | (built-in) | JSON serialization for SSE message payloads |
While SSE is a lightweight and effective protocol for unidirectional streaming, there are important constraints to be aware of when deploying it in real-world applications.
Under HTTP/1.1, browsers enforce a limit of approximately 6 simultaneous connections per domain. Each open SSE stream occupies one of these connections for as long as it remains active. This means that a single browser can maintain at most around 6 concurrent SSE connections to the same origin before new requests — including regular API calls, asset downloads, and additional SSE streams — are queued and blocked.
Because the connection limit is per domain, not per tab, multiple tabs open to the same application share the same pool of available connections. For example, if three tabs each open two SSE streams, all 6 connections are consumed and a fourth tab would be unable to establish any new HTTP connections to that domain until an existing stream is closed. This can lead to unresponsive pages and degraded user experience in applications that rely on SSE across multiple views.
The most effective way to overcome the HTTP/1.1 connection limit is to serve your application over HTTP/2. HTTP/2 multiplexes many streams over a single TCP connection, effectively removing the 6-connection bottleneck. With HTTP/2 enabled, browsers can maintain a much higher number of concurrent SSE streams to the same domain without blocking other requests.
To take advantage of HTTP/2 in ASP.NET Core, ensure your server is configured with HTTPS (HTTP/2 requires TLS in all major browsers) and that your hosting environment supports the protocol. Kestrel, the default ASP.NET Core web server, supports HTTP/2 out of the box.
For scenarios that require high concurrency (many simultaneous real-time connections) or bidirectional communication (both client and server need to send messages), WebSocket is the recommended alternative. WebSocket establishes a full-duplex channel over a single TCP connection, making it more efficient when the client also needs to send data back to the server.
In the .NET ecosystem, SignalR provides a high-level abstraction over WebSocket with automatic fallback transports, connection management, and group broadcasting. Consider WebSocket or SignalR when:
- Your application needs to support a large number of concurrent real-time connections per domain
- Clients need to send messages or commands back to the server
- You require features like connection grouping, presence tracking, or pub/sub patterns
- MDN — Server-Sent Events — Protocol overview, browser support, and usage guide for SSE
- MDN — EventSource API — Client-side JavaScript API for consuming SSE streams
- ASP.NET Core Minimal APIs — Official documentation for the Minimal API framework used in this project
- What's New in .NET 10 — Overview of features and improvements in .NET 10
- Bogus — Fake Data Generator — Library used to generate random notification data
- Swashbuckle.AspNetCore — Swagger UI and OpenAPI spec generation for ASP.NET Core