Skip to content

Real-time web / Engineering guide

Server-Sent Events Explained: How SSE Works and When to Use It

By ·

A report finishes. An import moves forward. A dashboard gets fresh data. Your server already knows something changed. How does the browser find out?

One answer is to ask again every few seconds. Another is to keep a connection open and let the server send an update when it has something to say. That second approach is where Server-Sent Events (SSE) fit: a small HTTP protocol with useful consequences for application design.

What are Server-Sent Events?

SSE lets a server send a sequence of text messages to a browser over a long-lived HTTP response. The browser opens the connection with EventSource; the server responds with Content-Type: text/event-stream. Each completed event becomes available to JavaScript without waiting for the response to finish.

The stream carries updates in one direction: server to browser. A user can still submit a form or make a separate HTTP request to start a job. SSE carries the progress back. This separation is often a natural fit for a product: commands go in, status updates come out.

Think of an export screen. The user clicks “Generate report,” the application queues the work, and the page starts showing progress. The user should not need to refresh, and the backend should not need to pretend every report takes the same amount of time.

How SSE works, step by step

BrowserGET /events →Server
Listening← event: progressJob running
Updated UI← event: completeJob finished
One HTTP response carries multiple events over time. The browser updates the relevant part of the page after each message.

A message is UTF-8 text with fields on separate lines. A blank line terminates the event. Here is an illustrative response body, with two events on the same stream:

retry: 3000
id: 41
event: progress
data: {"jobId":"export-7","percent":50}

id: 42
event: complete
data: {"jobId":"export-7","percent":100}

  • data carries the message. JSON is a useful convention; SSE itself does not require it.
  • event names the event. Without it, the browser dispatches a message event.
  • id sets the last event ID, which the browser can send as Last-Event-ID when reconnecting.
  • retry sets a reconnection delay in milliseconds.

These fields and parsing rules are defined in the WHATWG HTML standard for Server-Sent Events.

Reconnecting is not the same as recovering. An event ID does not store messages for you. If a missed update matters, your backend needs retained events and a replay policy, or a way for the client to fetch the current state after reconnecting.

A small JavaScript EventSource example

This browser example assumes your application serves the event stream above from /events. It is a protocol illustration, not a built-in Semitexa endpoint.

<p id="job-status" role="status">Connecting…</p>
<script>
const status = document.querySelector('#job-status');
const stream = new EventSource('/events');

stream.addEventListener('progress', (event) => {
  const update = JSON.parse(event.data);
  status.textContent = 'Export: ' + update.percent + '%';
});

stream.addEventListener('complete', () => {
  status.textContent = 'Your export is ready.';
  stream.close();
});

stream.onerror = () => {
  status.textContent = stream.readyState === EventSource.CLOSED
    ? 'Connection closed. Reload to try again.'
    : 'Connection interrupted. Reconnecting…';
};
</script>

Use addEventListener() for named events, and onmessage for unnamed messages. The native client normally retries interrupted connections; calling close() explicitly stops it. A production client should also validate payloads and make repeated updates safe. The MDN EventSource reference describes the client API and its connection states.

On the PHP side, encoding an event is straightforward: a name, a JSON payload, and the terminating blank line. Keeping many connections alive is the architectural decision. Avoid copying an infinite blocking loop into a normal page handler. Choose a runtime and streaming integration that can handle concurrent connections, disconnects, and cancellation.

SSE vs. WebSockets vs. polling

Start with the traffic your product needs. A progress indicator and a multiplayer game have very different conversations with their servers.

QuestionSSEWebSocketPolling
How do updates arrive?Server sends events over an HTTP response.Both peers send messages over an open connection.Client makes repeated HTTP requests.
Typical payloadText, often JSON or HTML.Text or binary messages.Whatever the HTTP endpoint returns.
Good starting pointJob progress, notifications, live dashboards.Frequent two-way interaction or binary traffic.Infrequent updates where a delay is acceptable.
Reconnect behaviorNative EventSource includes reconnection.Application or client library manages recovery.The next request is another opportunity to refresh.

The WebSocket API supports two-way messaging. Choose it when that capability earns its operational cost. Choose polling when simplicity and a relaxed freshness requirement make it sufficient. Choose SSE when most updates originate on the server and should reach an open page promptly.

A useful design exercise: write down every message your screen sends. If the browser mostly says “start this task” once, then listens for twenty progress updates, separate HTTP commands plus an SSE stream are worth evaluating. Measure under your own workload before making latency or capacity claims.

What to check before running SSE in production

Make sure each event reaches the browser promptly

A correct stream can still feel broken if a proxy buffers it. Review buffering, compression, and idle timeouts across the complete path. With NGINX, proxy_buffering off or an appropriate X-Accel-Buffering: no response header can control proxy buffering, depending on configuration. See the NGINX proxy buffering documentation. Test through the actual reverse proxy, not only directly against PHP.

Plan for quiet periods and multiple tabs

Comment lines such as : heartbeat, followed by a blank line, can keep an otherwise idle stream active. Send them more frequently than the shortest relevant idle timeout. HTTP/1.x browsers impose a small per-origin connection budget; HTTP/2 multiplexing helps, but still has negotiated stream limits. Prefer sharing a stream across features instead of opening one for every widget. See MDN’s SSE guide for heartbeat and connection-limit details.

Treat subscriptions as access to data

Authorize the connection and the events it receives. Native EventSource does not expose an option for arbitrary request headers. Same-origin cookie authentication is one practical choice; cross-origin credentials require deliberate CORS configuration. Avoid putting long-lived secrets in URLs. Decide what happens when a session expires or access to a resource changes.

Budget for slow clients and recovery

Put limits on pending output and connection lifetimes. Coalesce updates when only the latest value matters. For an import progress bar, receiving the current 80% state may be enough. For a business activity feed, missing items may be unacceptable. That difference should drive your retention, event IDs, replay behavior, and monitoring.

Where SSE fits in Semitexa Framework

Semitexa is a PHP framework built around typed payloads, handlers, and resources, with a Swoole runtime and server-rendered Twig views. Its SSR package includes deferred regions and live transport: a page can arrive with useful HTML, then receive server-produced updates for the parts that change.

This is useful when you want the server to remain responsible for presentation. An order status, report panel, or operational dashboard can use server-rendered regions rather than requiring a second implementation of the same rendering rules in browser code. SSE is the transport; your handlers and resources still determine what the user is allowed to see.

The Semitexa SSE demo shows backend-generated messages over a live connection and exposes the handler and client source. Opening its stream requires sign-in. For the broader rendering model, explore the SSR and live interface examples.

Start with one screen that has a clear need for server updates. Define the initial HTML, decide which region changes, and describe what should happen after a disconnect. Then use the live demo and source to evaluate the framework against those requirements.

Common questions about SSE

Can SSE replace WebSockets?

For server-to-browser updates, it may be enough. For frequent bidirectional messages or binary payloads, WebSockets may be a better fit. The decision follows your application's traffic, not a universal ranking.

Does SSE guarantee delivery?

No. Reconnection is a transport feature. Recovery requires application logic: retained events, replay, deduplication, or refreshing the current state.

Can I use SSE with PHP?

Yes. PHP can produce event-stream responses. The important questions are how your runtime handles long-lived connections and whether the complete delivery path flushes updates promptly.

Do I need a single-page application?

No. SSE can enhance a server-rendered page. Start with HTML, then update the region that needs fresh information. That is the approach the Semitexa rendering examples help you explore.

← Back to the Semitexa Blog

Ready to see it in a working PHP application? Read Streaming SSE with Semitexa for the live event and deferred HTML demos.