A Server-Sent Events Client for Delphi and .NET

· Components
A Server-Sent Events Client for Delphi and .NET

Server-Sent Events used to be the quiet cousin of WebSockets: a plain HTTP response that never ends, with the server writing one small text event after another. It is suddenly everywhere. The streamed answers of the LLM APIs arrive as SSE, MCP servers speak it over Streamable HTTP, and dashboards, notifications and job progress feeds use it because it goes through every proxy that lets HTTP through.

sgcWebSockets 2026.10 adds TsgcSSEClient, an EventSource client for Delphi, C++Builder and .NET. It reads the stream on a background thread, gives you every event with its type, data and id, reconnects on its own when the connection drops, and resumes with Last-Event-ID so nothing is lost in between. The component is not part of the current download. It arrives with the 2026.10 release, in every edition.

TsgcSSEClient receiving typed events, losing the connection and resuming with Last-Event-ID. Also on YouTube.

Why SSE Matters Now

An SSE stream is a text/event-stream response made of short blocks: an optional event: line with the type, one or more data: lines, an optional id:, and a blank line that ends the event. A server can also send retry: to tell the client how long to wait before reconnecting. The format is simple, but a correct client has to handle chunks that split a line anywhere, multi-line data, CR, LF and CRLF line endings, and a reconnect that remembers where it stopped. That is the part TsgcSSEClient does for you.

Quick Start

Add the unit sgcHTTP_SSE_Client, set the URL, handle OnEvent and call Open. Request headers such as a bearer token go into Headers, one Name: Value line each, and they are sent on every connection, reconnections included.

procedure TForm1.FormCreate(Sender: TObject);
begin
  oSSE := TsgcSSEClient.Create(nil);
  oSSE.URL := 'https://www.example.com/events';
  oSSE.Headers.Add('Authorization: Bearer ' + FToken);
  oSSE.OnEvent := OnSSEEvent;
  oSSE.OnError := OnSSEError;
  oSSE.Open;
end;

procedure TForm1.OnSSEEvent(Sender: TObject; const aEvent: TsgcSSEEvent);
begin
  if aEvent.EventType = 'alert' then
    ShowMessage(aEvent.Data)
  else
    Memo1.Lines.Add(aEvent.EventType + ' #' + aEvent.Id + ': ' + aEvent.Data);
end;

procedure TForm1.OnSSEError(Sender: TObject; const aError: string);
begin
  Memo1.Lines.Add('error: ' + aError);
end;

Every event is typed. EventType carries the event: name, so one handler can route alert, tick or any type your server invents. In a VCL or FireMonkey application the events are delivered to the main thread by default, so the handlers can touch the UI directly. NotifyEvents changes that. OnOpen and OnClose tell you when the stream starts and stops, and ReadyState reports sseConnecting, sseOpen or sseClosed.

The client reacts to the server the way a browser EventSource does:

Resume with Last-Event-ID

When the server gives its events an id: field, the client keeps the last one in LastEventId and sends it in the Last-Event-ID header on every reconnection. The server reads the header and continues after that event, so a dropped connection costs no message and repeats none. Inside one session this needs no code at all.

To resume after the application restarts, save LastEventId when you stop and restore it before Open. The property keeps its value after Close, and a value set before Open is sent with the very first request.

procedure TForm1.StartStream;
begin
  if FileExists('sse_last_id.txt') then
    oSSE.LastEventId := Trim(TFile.ReadAllText('sse_last_id.txt'));
  oSSE.Open;
end;

procedure TForm1.StopStream;
begin
  oSSE.Close;
  TFile.WriteAllText('sse_last_id.txt', oSSE.LastEventId);
end;

Resuming needs the server to cooperate: it must send id: with its events and honour the Last-Event-ID header.

A Reconnect Policy You Control

Reconnection is on by default, with a 3 second delay. ReconnectOptions turns that into whatever policy your server deserves: exponential backoff with a multiplier, a ceiling on the delay, random jitter so a thousand clients do not come back in the same millisecond, and a limit on attempts.

procedure TForm1.SetupReconnect;
begin
  oSSE.ReconnectOptions.Interval := 1000;
  oSSE.ReconnectOptions.Backoff := True;
  oSSE.ReconnectOptions.BackoffMultiplier := 2.0;
  oSSE.ReconnectOptions.MaxInterval := 30000;
  oSSE.ReconnectOptions.Jitter := 0.2;
  oSSE.ReconnectOptions.MaxAttempts := 10;
  oSSE.OnReconnect := OnSSEReconnect;
end;

MaxAttempts counts consecutive failures, and the counter starts again every time a connection opens, so a client that runs for weeks with the odd network hiccup never runs out of attempts. The server has a say as well: a retry: field replaces Interval for the rest of the session. The last word is yours, in OnReconnect, which fires before every attempt with the calculated delay:

procedure TForm1.OnSSEReconnect(Sender: TObject; aAttempt: Integer;
  var aDelay: Integer; var aCancel: Boolean);
begin
  // stop after the fifth failed attempt, otherwise wait at least 2 seconds
  if aAttempt > 5 then
    aCancel := True
  else if aDelay < 2000 then
    aDelay := 2000;
end;

TLS, Proxy and Authentication

The stream is read with a TsgcHTTP1Client, and OnBeforeConnect hands it to you before every connection attempt. Anything the HTTP client can do, the SSE client can do: TLS options, a proxy, basic authentication or a longer read timeout.

procedure TForm1.OnSSEBeforeConnect(Sender: TObject;
  const aHTTP: TsgcHTTP1Client);
begin
  aHTTP.TLSOptions.Version := tls1_2;
  aHTTP.Proxy.Enabled := True;
  aHTTP.Proxy.Host := '192.168.1.10';
  aHTTP.Proxy.Port := 8080;
  aHTTP.ReadTimeout := 60000;
end;

OnBeforeConnect and OnReconnect always run on the background thread, so keep the UI out of them.

The Parser for LLM Streams and MCP

Often you do not need a long-lived EventSource at all. An LLM chat completion with stream: true is a POST whose response is SSE, and an MCP Streamable HTTP response can be SSE too. For those, the parser inside the client is public: TsgcSSEParser. Feed it raw bytes or text in chunks of any size, exactly as the network delivers them, and it fires OnEvent for every complete event and OnRetry for every valid retry: field. A chunk can end in the middle of a line or of a UTF-8 character, and the parser keeps the rest until the next call.

procedure TForm1.ParseStream;
begin
  FParser := TsgcSSEParser.Create;
  FParser.OnEvent := OnParserEvent;
  // chunks arrive as the network delivers them, split anywhere
  FParser.Feed('event: content_block_delta'#10'data: {"delta":{"text":"Hel');
  FParser.Feed('lo"}}'#10#10'event: message_stop'#10);
  FParser.Feed(TEncoding.UTF8.GetBytes('data: {}'#10#10));
  // start again for the next response
  FParser.Reset;
end;

procedure TForm1.OnParserEvent(Sender: TObject; const aEvent: TsgcSSEEvent);
begin
  Memo1.Lines.Add(aEvent.EventType + ' ' + aEvent.Data);
end;

Those three chunks produce two events: content_block_delta with the complete JSON {"delta":{"text":"Hello"}}, and message_stop. Reset discards any half-read event before the next response, and LastEventId tells you the last id the parser saw.

C# for .NET

The .NET edition of sgcWebSockets has the same component with the same names, in the esegece.sgcWebSockets namespace. Headers is a list of Name: Value strings and the events are regular .NET events.

using esegece.sgcWebSockets;

string token = args.Length > 0 ? args[0] : "";

var sse = new TsgcSSEClient();
sse.URL = "https://www.example.com/events";
sse.Headers.Add("Authorization: Bearer " + token);
if (File.Exists("sse_last_id.txt"))
    sse.LastEventId = File.ReadAllText("sse_last_id.txt").Trim();
sse.ReconnectOptions.Backoff = true;
sse.ReconnectOptions.MaxInterval = 30000;
sse.OnEvent += (sender, e) => Console.WriteLine($"{e.EventType} #{e.Id}: {e.Data}");
sse.OnError += (sender, error) => Console.WriteLine("error: " + error);
sse.Open();

Console.ReadLine();
sse.Close();
File.WriteAllText("sse_last_id.txt", sse.LastEventId);

TsgcSSEParser is there too, with Feed overloads for a byte[], a slice of one, or a string.

Try the Demos

Both editions ship a demo that runs fully offline. It hosts a small SSE server inside the same application on 127.0.0.1, port 5580, which streams numbered events with an id:, rotates the types message, tick and alert, sends a retry: field and resumes after the Last-Event-ID it receives. Drop the connection and watch the client reconnect and continue from the next id, with no gap and no duplicate.

Availability

TsgcSSEClient and TsgcSSEParser arrive in sgcWebSockets 2026.10, for Delphi, C++Builder and .NET, in every edition. They are not in the version you can download today. When 2026.10 is out, it will be on the download page, and the component will be registered in the SGC HTTP palette tab.

The full reference, with every property, event and the resume guide, is in the TsgcSSEClient help. For everything else sgcWebSockets does with Server-Sent Events, see the SSE product page.

Questions, feedback or migration help? Get in touch. You will get a reply from the people who wrote the code.