Ofline
If you have ever built a dashboard or an operations screen, you know the problem. You want the numbers on screen to reflect what is actually happening on the server, but you also do not want to write a polling loop that fires a request every second and hammers your backend whether or not anything has changed. There has to be a better way.
For a large class of update-heavy scenarios, that better way is Server-Sent Events (SSE): a standard HTTP-based way for a server to keep one connection open and stream updates to the client as they happen. RAD Studio 13.1 recently added first-class support for this on both server and client sides, so we built a small proof of concept to show what that looks like end to end.
Table of Contents
The idea behind SSE is straightforward. The client opens a normal HTTP connection to the server and keeps it open. Instead of the server sending a response and closing like a regular web page, it holds the connection and pushes data down to the user’s browser or application whenever something worth reporting happens. The client processes each chunk as it arrives and updates the UI accordingly.
Where SSE shines is precisely this kind of scenario: the server knows things the client needs to see, and the client’s job is mostly to display them. Live metrics, graph data, job progress, notifications, log tails… For any of those, SSE is a natural fit and considerably simpler to implement than a full WebSocket setup.
WebSockets are really useful when you need high-frequency two-way communication on the same channel as the initial content, or when you are building something with a complex bidirectional protocol. But for server-to-client updates, SSE is usually the cleaner, more optimal choice:
Let’s look at the code required to make SSE work in RAD Studio 13.1. Thanks to the new System.Net.HttpSse unit and WebBroker updates, the plumbing is remarkably simple.
To turn a standard HTTP response into a streaming SSE connection, you upgrade the response using TWebResponseStream.BeginEventsStream. From there, you just write your events in a loop as long as the client remains connected.
procedure TWebModule1.WebModule1EventsAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var Stream: TWebResponseStream; begin Handled := True; // 1. Upgrade the standard HTTP response to an active SSE stream Stream := TWebResponseStream.BeginEventsStream(Response, 15); try while Stream.Connected do begin // 2. Write and flush a single event Stream.WriteEvent('message'); Stream.WriteData('Hello from Delphi SSE!'); Stream.EndEvent; // 3. Yield to keep the loop from eating the CPU Sleep(100); end; except on E: Exception do ; // Handle client disconnects gracefully end; end;
On the client side, you use the new THTTPEventSource. It handles the HTTP connection in the background and queues up incoming events. Your job is simply to pull them off the queue and read the data.
procedure TForm1.HandleSseMessages(ASender: THTTPEventSource); var Ev: THTTPEvent; begin // 1. Grab the next available event from the queue Ev := ASender.GetEvent; while Assigned(Ev) do begin try // 2. Process the event payload AppendLog(Format('Event: %s | Data: %s', [Ev.Event, Ev.Data.Text])); finally Ev.Free; end; // 3. Keep pulling until the queue is empty Ev := ASender.GetEvent; end; end;
Now that you see the underlying mechanics, let’s look at how we take this foundation and scale it up into a more advanced system with custom timings, background threads, and multiple event channels.
The demo we built around this has three pieces: a Delphi WebBroker console server, a browser dashboard page served from that same server using WebStencils, and a native FireMonkey desktop app. All three run at the same time, and the browser and FMX clients both connect to the same SSE endpoint.
The server exposes a /events endpoint. When a client connects, the server keeps that connection open and starts pushing named events in a loop. Three event types flow through the stream:
What makes it concrete is the job trigger. Hit the Start Job button from either client and both UIs start receiving progress updates simultaneously. The browser progress bar and the FMX progress bar move in step with each other because they are both reading from the same server-side state.
The server side is built using WebBroker. The SSE endpoint uses TWebResponseStream.BeginEventsStream to open the streaming response and then sits in a loop while the connection is alive, emitting events as their cadence timers tick over. Each client can tune its own update frequency with query parameters, so one client can request heartbeats every second while another asks for them every five seconds. The server clamps the values to a sensible range and handles the rest.
That is the essence of it: open the SSE stream, write a named event with JSON payload (or any other data), and end the frame.
The dashboard and job state are shared across all connections through a pair of thread-safe singleton objects. That is what keeps the numbers consistent: every client that asks for a dashboard snapshot gets the same underlying values, not a per-connection random walk that drifts out of sync.
On the client side, the browser uses the built-in EventSource API. The FMX app uses THTTPEventSource from System.Net.HttpSse, which was added in RAD Studio 13.1. Both clients listen for the same event names and update their UI elements when those events arrive. The server does not know or care whether a client is a browser or a Delphi app. The event format is the same either way.
It is worth being clear about something: SSE is an open web standard. RAD Studio gives you a native way to create and use them, but the underlying format of how server side events work is understood by every HTTP stack and every major browser. That means a Delphi server can push events to a JavaScript client, a Python client, a Go client, or anything else that speaks HTTP. Equally, a Delphi application using THTTPEventSource can consume SSE streams from non-Delphi services without any special adaptation.
That last point is useful in practice right now. Several major public production APIs already stream their responses over SSE:
A Delphi app with THTTPEventSource can connect to any of those APIs directly, without writing a custom HTTP streaming parser to process the SSE results.
There is a related topic worth mentioning. The Model Context Protocol (MCP) has become a common standard for connecting AI tools, editors, and agents to external capabilities. The original MCP HTTP transport was built on SSE, and while the current specification has moved toward a more flexible transport called Streamable HTTP, that newer transport still uses SSE for the streaming leg of its communication. In practical terms, a lot of real-world MCP implementations in use today are still SSE-based, and even the newer ones rely on the same streaming fundamentals.
The relevance here is that if you are thinking about building MCP integrations in Delphi, or connecting to third-party MCP servers that others are developing, the streaming knowledge from this demo transfers directly. You would not need to build your own HTTP streaming layer from scratch. The same THTTPEventSource plumbing this PoC demonstrates is what you would reach for.
This demo is intentionally small. The dashboard KPIs are randomly simulated and the background job is a timer. The point is not the data, it is the plumbing. As we show in this demo, once you understand how server sent events flow from a Delphi WebBroker endpoint to a browser page and a FMX app simultaneously, replacing the mock data with real business events from your own services is a straightforward step.
If you have been looking for a practical way to add real-time updates to a RAD Studio application without taking on significant architectural complexity, this is a solid place to start.
For a large class of update-heavy scenarios, that better way is Server-Sent Events (SSE): a standard HTTP-based way for a server to keep one connection open and stream updates to the client as they happen. RAD Studio 13.1 recently added first-class support for this on both server and client sides, so we built a small proof of concept to show what that looks like end to end.
Table of Contents
What does SSE mean?
The idea behind SSE is straightforward. The client opens a normal HTTP connection to the server and keeps it open. Instead of the server sending a response and closing like a regular web page, it holds the connection and pushes data down to the user’s browser or application whenever something worth reporting happens. The client processes each chunk as it arrives and updates the UI accordingly.
Where SSE shines is precisely this kind of scenario: the server knows things the client needs to see, and the client’s job is mostly to display them. Live metrics, graph data, job progress, notifications, log tails… For any of those, SSE is a natural fit and considerably simpler to implement than a full WebSocket setup.
WebSockets are really useful when you need high-frequency two-way communication on the same channel as the initial content, or when you are building something with a complex bidirectional protocol. But for server-to-client updates, SSE is usually the cleaner, more optimal choice:
- Runs over plain HTTP, no special protocol required.
- Built-in reconnection in every browser, and in RAD Studio using THTTPEventSource.
- Named event types keep client-side logic simple and explicit.
SSE in RAD Studio: The Fundamentals
Let’s look at the code required to make SSE work in RAD Studio 13.1. Thanks to the new System.Net.HttpSse unit and WebBroker updates, the plumbing is remarkably simple.
The Server Side (WebBroker)
To turn a standard HTTP response into a streaming SSE connection, you upgrade the response using TWebResponseStream.BeginEventsStream. From there, you just write your events in a loop as long as the client remains connected.
procedure TWebModule1.WebModule1EventsAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var Stream: TWebResponseStream; begin Handled := True; // 1. Upgrade the standard HTTP response to an active SSE stream Stream := TWebResponseStream.BeginEventsStream(Response, 15); try while Stream.Connected do begin // 2. Write and flush a single event Stream.WriteEvent('message'); Stream.WriteData('Hello from Delphi SSE!'); Stream.EndEvent; // 3. Yield to keep the loop from eating the CPU Sleep(100); end; except on E: Exception do ; // Handle client disconnects gracefully end; end;
| procedure TWebModule1.WebModule1EventsAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); Stream: TWebResponseStream; Handled := True; // 1. Upgrade the standard HTTP response to an active SSE stream Stream := TWebResponseStream.BeginEventsStream(Response, 15); while Stream.Connected do begin // 2. Write and flush a single event Stream.WriteEvent('message'); Stream.WriteData('Hello from Delphi SSE!'); Stream.EndEvent; // 3. Yield to keep the loop from eating the CPU Sleep(100); end; except on E: Exception do ; // Handle client disconnects gracefully |
The Client Side (THTTPEventSource)
On the client side, you use the new THTTPEventSource. It handles the HTTP connection in the background and queues up incoming events. Your job is simply to pull them off the queue and read the data.
procedure TForm1.HandleSseMessages(ASender: THTTPEventSource); var Ev: THTTPEvent; begin // 1. Grab the next available event from the queue Ev := ASender.GetEvent; while Assigned(Ev) do begin try // 2. Process the event payload AppendLog(Format('Event: %s | Data: %s', [Ev.Event, Ev.Data.Text])); finally Ev.Free; end; // 3. Keep pulling until the queue is empty Ev := ASender.GetEvent; end; end;
| procedure TForm1.HandleSseMessages(ASender: THTTPEventSource); Ev: THTTPEvent; // 1. Grab the next available event from the queue Ev := ASender.GetEvent; while Assigned(Ev) do try // 2. Process the event payload AppendLog(Format('Event: %s | Data: %s', [Ev.Event, Ev.Data.Text])); finally Ev.Free; end; // 3. Keep pulling until the queue is empty Ev := ASender.GetEvent; |
Now that you see the underlying mechanics, let’s look at how we take this foundation and scale it up into a more advanced system with custom timings, background threads, and multiple event channels.
The Server Side Events (SSE) demo
The demo we built around this has three pieces: a Delphi WebBroker console server, a browser dashboard page served from that same server using WebStencils, and a native FireMonkey desktop app. All three run at the same time, and the browser and FMX clients both connect to the same SSE endpoint.
The server exposes a /events endpoint. When a client connects, the server keeps that connection open and starts pushing named events in a loop. Three event types flow through the stream:
- heartbeat — fires on a configurable interval to keep the stream alive and confirm the server is still there.
- dashboard — carries live KPI snapshots: active users, sales total, and average order value.
- progress — appears when a background job is running, updated as the job advances.
What makes it concrete is the job trigger. Hit the Start Job button from either client and both UIs start receiving progress updates simultaneously. The browser progress bar and the FMX progress bar move in step with each other because they are both reading from the same server-side state.
How does our SSE demo fit together on the server?
The server side is built using WebBroker. The SSE endpoint uses TWebResponseStream.BeginEventsStream to open the streaming response and then sits in a loop while the connection is alive, emitting events as their cadence timers tick over. Each client can tune its own update frequency with query parameters, so one client can request heartbeats every second while another asks for them every five seconds. The server clamps the values to a sensible range and handles the rest.
That is the essence of it: open the SSE stream, write a named event with JSON payload (or any other data), and end the frame.
The dashboard and job state are shared across all connections through a pair of thread-safe singleton objects. That is what keeps the numbers consistent: every client that asks for a dashboard snapshot gets the same underlying values, not a per-connection random walk that drifts out of sync.
On the client side, the browser uses the built-in EventSource API. The FMX app uses THTTPEventSource from System.Net.HttpSse, which was added in RAD Studio 13.1. Both clients listen for the same event names and update their UI elements when those events arrive. The server does not know or care whether a client is a browser or a Delphi app. The event format is the same either way.
Server Side Events are not a Delphi-specific technology
It is worth being clear about something: SSE is an open web standard. RAD Studio gives you a native way to create and use them, but the underlying format of how server side events work is understood by every HTTP stack and every major browser. That means a Delphi server can push events to a JavaScript client, a Python client, a Go client, or anything else that speaks HTTP. Equally, a Delphi application using THTTPEventSource can consume SSE streams from non-Delphi services without any special adaptation.
That last point is useful in practice right now. Several major public production APIs already stream their responses over SSE:
A Delphi app with THTTPEventSource can connect to any of those APIs directly, without writing a custom HTTP streaming parser to process the SSE results.
How does MCP fit in with SSE?
There is a related topic worth mentioning. The Model Context Protocol (MCP) has become a common standard for connecting AI tools, editors, and agents to external capabilities. The original MCP HTTP transport was built on SSE, and while the current specification has moved toward a more flexible transport called Streamable HTTP, that newer transport still uses SSE for the streaming leg of its communication. In practical terms, a lot of real-world MCP implementations in use today are still SSE-based, and even the newer ones rely on the same streaming fundamentals.
The relevance here is that if you are thinking about building MCP integrations in Delphi, or connecting to third-party MCP servers that others are developing, the streaming knowledge from this demo transfers directly. You would not need to build your own HTTP streaming layer from scratch. The same THTTPEventSource plumbing this PoC demonstrates is what you would reach for.
What comes next?
This demo is intentionally small. The dashboard KPIs are randomly simulated and the background job is a timer. The point is not the data, it is the plumbing. As we show in this demo, once you understand how server sent events flow from a Delphi WebBroker endpoint to a browser page and a FMX app simultaneously, replacing the mock data with real business events from your own services is a straightforward step.
If you have been looking for a practical way to add real-time updates to a RAD Studio application without taking on significant architectural complexity, this is a solid place to start.