{"id":12333,"date":"2026-04-12T10:02:31","date_gmt":"2026-04-12T10:02:31","guid":{"rendered":"https:\/\/putridparrot.com\/blog\/?p=12333"},"modified":"2026-04-12T10:08:46","modified_gmt":"2026-04-12T10:08:46","slug":"lets-talk-server-sent-events","status":"publish","type":"post","link":"https:\/\/putridparrot.com\/blog\/lets-talk-server-sent-events\/","title":{"rendered":"Let&#8217;s talk Server-Sent Events"},"content":{"rendered":"<p>Server-Sent Events (SSE) are a lightweight streaming\/push technology for pushing updates to a web client over a single long-live HTTP connection.<\/p>\n<p>For example as use case where your web client connects to a server and periodically receives events or notifications from the server. Alternates to SSE might be long polling the server for updates, web sockets, Signal R etc. SSE is simpler than setting up web sockets and for C#\/.NET developers can be seen to be similar to asynchronous streams.<\/p>\n<p>All modern browser should support SSE using EventSource. SSE also supports automatic reconnection but one key thing to remember is this is only for <strong>one-way<\/strong> updates (unlike Signal R).<\/p>\n<p><strong>ASP.NET Sample<\/strong><\/p>\n<p>Let&#8217;s create an ASP.NET server sample with an endpoint <em>\/events<\/em> of type <em>text\/event-stream<\/em>. This will just look and send &#8220;ticks&#8221; to a client.<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\napp.MapGet(&quot;\/events&quot;, async (HttpContext context, CancellationToken cancellationToken) =&gt;\r\n{\r\n    context.Response.ContentType = &quot;text\/event-stream&quot;;\r\n    context.Response.Headers.CacheControl = &quot;no-cache&quot;;\r\n\r\n    try\r\n    {\r\n        for (var i = 1; i &lt;= 20; i++)\r\n        {\r\n            var payload = JsonSerializer.Serialize(new\r\n            {\r\n                id = i,\r\n                message = $&quot;Server tick {i}&quot;,\r\n                sentAtUtc = DateTimeOffset.UtcNow\r\n            });\r\n\r\n            await context.Response.WriteAsync($&quot;id: {i}\\n&quot;, cancellationToken);\r\n            await context.Response.WriteAsync(&quot;event: tick\\n&quot;, cancellationToken);\r\n            await context.Response.WriteAsync($&quot;data: {payload}\\n\\n&quot;, cancellationToken);\r\n            await context.Response.Body.FlushAsync(cancellationToken);\r\n\r\n            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);\r\n        }\r\n    }\r\n    catch (OperationCanceledException)\r\n    {\r\n    }\r\n});\r\n<\/pre>\n<p>Pretty simple. As mentioned we need to set the ContentType to <em>text\/event-stream<\/em>. SSE must never be cached, hence <em>context.Response.Headers.CacheControl = &#8220;no-cache&#8221;<\/em>. The handler should not return until all responses are sent, i.e. it goes into a loop or similar and writes responses. <\/p>\n<p>It&#8217;s also important to note that the SSE spec requires the following wire format<\/p>\n<ul>\n<li><strong>id:<\/strong> An optional event ID<\/li>\n<li><strong>event:<\/strong> An optional even name<\/li>\n<li><strong>datae:<\/strong> The actual payload<\/li>\n<li><strong>Ends with a blank line:<\/strong> Notice of the line setting the data we have two new lines, i.e. one blank line. Without this the browser will not dispatch the event<\/li>\n<li><strong>Flush after each event:<\/strong> This will force the server to push the event immediately instead of buffering, otherwise the client might receive events in chunks<\/li>\n<\/ul>\n<p><strong>C# client code<\/strong><\/p>\n<p>Let&#8217;s look at what&#8217;s required for a C# client to interact with our server.<\/p>\n<p>The HttpClient looks like this<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nusing var httpClient = new HttpClient\r\n{\r\n    BaseAddress = new Uri(&quot;http:\/\/localhost:5127&quot;)\r\n};\r\n<\/pre>\n<p>Now we&#8217;ll created the request tot he <em>\/events<\/em> endpoint and accept the <em>text\/event-stream<\/em> i.e.<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nusing var cancellation = new CancellationTokenSource();\r\n\r\nusing var request = new HttpRequestMessage(HttpMethod.Get, &quot;\/events&quot;);\r\nrequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(&quot;text\/event-stream&quot;));\r\n\r\nusing var response = await httpClient.SendAsync(\r\n    request,\r\n    HttpCompletionOption.ResponseHeadersRead,\r\n    cancellation.Token);\r\n\r\nresponse.EnsureSuccessStatusCode();\r\n<\/pre>\n<p>Okay, seo we&#8217;ve configured things and called the endpoint, now let&#8217;s look at how we might read the SSE<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\nawait using var stream = await response.Content.ReadAsStreamAsync(cancellation.Token);\r\nusing var reader = new StreamReader(stream);\r\n\r\nwhile (!cancellation.IsCancellationRequested)\r\n{\r\n  var line = await reader.ReadLineAsync(cancellation.Token);\r\n\r\n  if (line is null)\r\n  {\r\n    break;\r\n  }\r\n\r\n  if (line.Length == 0)\r\n  {\r\n    if (eventDataBuilder.Length &gt; 0)\r\n    {\r\n      Console.WriteLine($&quot;&#x5B;{DateTime.Now:T}] id={eventId}, event={eventName}, data={eventDataBuilder}&quot;);\r\n    }\r\n\r\n    eventId = string.Empty;\r\n    eventName = string.Empty;\r\n    eventDataBuilder.Clear();\r\n    continue;\r\n  }\r\n\r\n  if (line.StartsWith(&quot;id: &quot;, StringComparison.Ordinal))\r\n  {\r\n    eventId = line&#x5B;4..];\r\n    continue;\r\n  }\r\n\r\n  if (line.StartsWith(&quot;event: &quot;, StringComparison.Ordinal))\r\n  {\r\n    eventName = line&#x5B;7..];\r\n    continue;\r\n  }\r\n\r\n  if (line.StartsWith(&quot;data: &quot;, StringComparison.Ordinal))\r\n  {\r\n    if (eventDataBuilder.Length &gt; 0)\r\n    {\r\n      eventDataBuilder.Append(&#039;\\n&#039;);\r\n    }\r\n\r\n    eventDataBuilder.Append(line&#x5B;6..]);\r\n}\r\n<\/pre>\n<p>In the example code, above, we read each line from the SSE and get each part until we&#8217;re ready to put the read data together to write to the console.<\/p>\n<p><strong>Typescript client<\/strong><\/p>\n<p>The following example is a React Typescript implementation of a simply UI with connect buttons etc. to connect to the events URL, using an EventSource we add a listener to and output the SSE&#8217;s as they arrive.<\/p>\n<pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\r\ntype TickEvent = {\r\n  id: number\r\n  message: string\r\n  sentAtUtc: string\r\n}\r\n\r\nconst maxItems = 50\r\n\r\nfunction App() {\r\n  const &#x5B;events, setEvents] = useState&lt;TickEvent&#x5B;]&gt;(&#x5B;])\r\n  const &#x5B;isConnected, setIsConnected] = useState(false)\r\n  const &#x5B;status, setStatus] = useState(&#039;Disconnected&#039;)\r\n  const eventSourceRef = useRef&lt;EventSource | null&gt;(null)\r\n\r\n  useEffect(() =&gt; {\r\n    return () =&gt; {\r\n      eventSourceRef.current?.close()\r\n      eventSourceRef.current = null\r\n    }\r\n  }, &#x5B;])\r\n\r\n  const connect = () =&gt; {\r\n    if (eventSourceRef.current) {\r\n      return\r\n    }\r\n\r\n    setStatus(&#039;Connecting...&#039;)\r\n\r\n    const eventSource = new EventSource(import.meta.env.VITE_SSE_URL ?? &#039;\/events&#039;)\r\n    eventSourceRef.current = eventSource\r\n\r\n    eventSource.onopen = () =&gt; {\r\n      setIsConnected(true)\r\n      setStatus(&#039;Connected&#039;)\r\n    }\r\n\r\n    eventSource.addEventListener(&#039;tick&#039;, (event) =&gt; {\r\n      const messageEvent = event as MessageEvent&lt;string&gt;\r\n\r\n      try {\r\n        const payload = JSON.parse(messageEvent.data) as TickEvent\r\n        setEvents((previous) =&gt; &#x5B;payload, ...previous].slice(0, maxItems))\r\n      } catch {\r\n        setStatus(&#039;Received malformed event payload&#039;)\r\n      }\r\n    })\r\n\r\n    eventSource.onerror = () =&gt; {\r\n      setStatus(&#039;Connection lost or closed by server&#039;)\r\n      setIsConnected(false)\r\n      eventSource.close()\r\n      eventSourceRef.current = null\r\n    }\r\n  }\r\n\r\n  const disconnect = () =&gt; {\r\n    eventSourceRef.current?.close()\r\n    eventSourceRef.current = null\r\n    setIsConnected(false)\r\n    setStatus(&#039;Disconnected&#039;)\r\n  }\r\n\r\n  const clear = () =&gt; setEvents(&#x5B;])\r\n\r\n  return (\r\n    &lt;main className=&quot;app&quot;&gt;\r\n      &lt;h1&gt;Server Sent Events Demo&lt;\/h1&gt;\r\n      &lt;p className=&quot;subtitle&quot;&gt;React + TypeScript client for the ServerSentEventsSample API&lt;\/p&gt;\r\n\r\n      &lt;div className=&quot;controls&quot;&gt;\r\n        &lt;button onClick={connect} disabled={isConnected}&gt;Connect&lt;\/button&gt;\r\n        &lt;button onClick={disconnect} disabled={!isConnected}&gt;Disconnect&lt;\/button&gt;\r\n        &lt;button onClick={clear} disabled={events.length === 0}&gt;Clear&lt;\/button&gt;\r\n      &lt;\/div&gt;\r\n\r\n      &lt;p className=&quot;status&quot;&gt;\r\n        Status: &lt;strong&gt;{status}&lt;\/strong&gt;\r\n      &lt;\/p&gt;\r\n\r\n      &lt;ul className=&quot;event-list&quot;&gt;\r\n        {events.length === 0 &amp;&amp; &lt;li className=&quot;empty&quot;&gt;No events yet.&lt;\/li&gt;}\r\n        {events.map((item) =&gt; (\r\n          &lt;li key={`${item.id}-${item.sentAtUtc}`}&gt;\r\n            &lt;div className=&quot;row&quot;&gt;\r\n              &lt;span className=&quot;id&quot;&gt;#{item.id}&lt;\/span&gt;\r\n              &lt;span&gt;{item.message}&lt;\/span&gt;\r\n            &lt;\/div&gt;\r\n            &lt;time dateTime={item.sentAtUtc}&gt;\r\n              {new Date(item.sentAtUtc).toLocaleTimeString()}\r\n            &lt;\/time&gt;\r\n          &lt;\/li&gt;\r\n        ))}\r\n      &lt;\/ul&gt;\r\n    &lt;\/main&gt;\r\n  )\r\n}\r\n<\/pre>\n<p><strong>Source Code<\/strong><\/p>\n<p>Full code for these examples is available on <a href=\"https:\/\/github.com\/putridparrot\/blog-projects\/tree\/master\/ServerSentEventsSample\" target=\"_blank\">Github<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Server-Sent Events (SSE) are a lightweight streaming\/push technology for pushing updates to a web client over a single long-live HTTP connection. For example as use case where your web client connects to a server and periodically receives events or notifications from the server. Alternates to SSE might be long polling the server for updates, web [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[128,3],"tags":[],"class_list":["post-12333","post","type-post","status-publish","format-standard","hentry","category-asp-net","category-c"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/12333","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/comments?post=12333"}],"version-history":[{"count":5,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/12333\/revisions"}],"predecessor-version":[{"id":12339,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/posts\/12333\/revisions\/12339"}],"wp:attachment":[{"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/media?parent=12333"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/categories?post=12333"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/putridparrot.com\/blog\/wp-json\/wp\/v2\/tags?post=12333"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}