Which real-time mechanism fits which REST API use case, and why the choice is rarely trivial
Once a REST API needs to deliver real-time updates, such as live order status, notifications, or price changes, teams face a choice between Server-Sent Events and WebSockets. Both solve the problem that classic HTTP polling creates unnecessary latency and load, but they differ fundamentally in protocol overhead, direction of data flow, and implementation complexity, which is why the choice significantly affects the long-term maintenance cost of an API extension.
Table of Contents
- 1. Why polling doesn't fit real-time requirements
- 2. Server-Sent Events: simple unidirectional server push over HTTP
- 3. WebSockets: a full bidirectional protocol with its own overhead
- 4. The practical PHP-FPM limitation with long-lived connections
- 5. Mercure as a pragmatic SSE hub in the Symfony ecosystem
- 6. Concrete decision criteria for the choice
- 7. Hybrid approaches: REST API plus SSE or WebSocket as a supplement
- 8. Heartbeats and keepalive against silent connection drops
- 9. SSE and WebSockets compared side by side
- 10. Summary
- 11. FAQ
1. Why polling doesn't fit real-time requirements
Classic HTTP polling, where a client repeatedly asks for new data at fixed intervals, creates an uncomfortable tradeoff between latency and load: short intervals deliver timely updates but cause high server load through mostly empty responses, while long intervals reduce load but let real changes reach the client only after noticeable delay. For use cases like live price changes, chat notifications, or order status updates, this delay is often unacceptable, especially when users expect an immediate reaction from the interface.
Server-Sent Events and WebSockets solve this problem by having the server actively send data to already connected clients (server push), instead of waiting for repeated requests. Both mechanisms keep a long-lived connection open, but differ fundamentally in how this connection works and what can be transmitted over it.
2. Server-Sent Events: simple unidirectional server push over HTTP
Server-Sent Events (SSE) build on a normal, long-lived HTTP connection over which the server continuously sends text events in text/event-stream format to the client, without the client ever sending data back over the same connection. This simplicity is a key advantage: SSE works over normal HTTP infrastructure (reverse proxies, load balancers, firewalls) without needing a protocol upgrade like WebSockets, and the native EventSource browser API automatically handles reconnect logic on connection loss.
The downside of this simplicity is pure unidirectionality: a client wanting to react to a received event (say, confirming a chat message) needs to send a separate, classic HTTP request to a different endpoint, instead of responding over the same connection. For pure broadcast scenarios, where only the server initiates, this isn't a drawback; for interactive real-time communication, it is.
3. WebSockets: a full bidirectional protocol with its own overhead
WebSockets start with an HTTP upgrade handshake but then switch entirely to their own binary framing protocol, over which both server and client can send messages in either direction at any time, without each message needing its own request-response cycle like HTTP. This true bidirectionality makes WebSockets the right choice for use cases like chat applications, collaborative editors, or multiplayer interactions, where both sides need to communicate as equals.
This feature set comes with higher complexity: WebSocket connections need their own reconnect logic (the EventSource API provides this natively for SSE, but it has to be implemented manually for WebSockets), their own understanding of load balancing (classic round-robin load balancers don't map directly to long-lived, stateful connections), and often additional infrastructure like a dedicated WebSocket server or a hub like Mercure.
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\StreamedResponse;
final class OrderStatusSseController
{
public function stream(int $orderId): StreamedResponse
{
$response = new StreamedResponse(function () use ($orderId) {
while (true) {
$status = $this->orderStatusRepository->getCurrentStatus($orderId);
echo "data: " . json_encode(['status' => $status]) . "\n\n";
ob_flush();
flush();
if ($status === 'delivered' || connection_aborted()) {
break;
}
sleep(2);
}
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('Cache-Control', 'no-cache');
$response->headers->set('X-Accel-Buffering', 'no');
return $response;
}
}
4. The practical PHP-FPM limitation with long-lived connections
An often underestimated practical problem with SSE and WebSockets in a classic PHP-FPM environment is that every open, long-lived connection blocks an entire PHP-FPM worker process for the full duration of the connection, which quickly exhausts available workers with thousands of concurrent connections. Pure PHP-FPM implementations of SSE or WebSockets therefore practically scale only up to a limited number of concurrent users.
For production-grade scaling, a dedicated real-time gateway like Mercure (well integrated into the Symfony ecosystem) or a separate Node.js-based WebSocket server is usually used, managing connections efficiently with an event-based, non-blocking model instead of tying each connection to a full PHP process. The PHP backend code then publishes events to this hub, instead of holding connections itself.
5. Mercure as a pragmatic SSE hub in the Symfony ecosystem
Mercure is an open standard and a dedicated hub program that efficiently manages SSE connections outside PHP-FPM, while the Symfony application merely publishes updates to the hub via a simple HTTP API, which the hub then distributes to all subscribed clients. This approach combines the simplicity of SSE on the client side with production-ready, scalable infrastructure on the server side, without the Symfony application itself having to manage long-lived connections.
For WebSocket-based use cases, no directly comparable, equally tightly integrated standard tool exists within the Symfony ecosystem, which is why teams more often fall back on external solutions like Soketi, Pusher, or their own Node.js-based infrastructure, noticeably increasing integration effort compared to Mercure.
6. Concrete decision criteria for the choice
The decision between SSE and WebSockets should primarily be based on communication direction: if only the server pushes updates to clients, without clients needing to respond over the same connection (notifications, live status, price changes, dashboards), SSE is the simpler, more robust, and less infrastructure-heavy choice. If both sides need to communicate as equals with low latency (chat, collaborative editing, multiplayer), WebSocket is practically the only option.
Another practical criterion is behavior around proxy-heavy infrastructure: SSE works more reliably behind restrictive firewalls and corporate proxies that sometimes block WebSocket upgrades, because SSE appears as a normal, long-lived HTTP connection, while WebSocket connections can be explicitly identified as such and potentially blocked.
7. Hybrid approaches: REST API plus SSE or WebSocket as a supplement
In practice, SSE and WebSockets rarely fully replace the classic REST API, but rather supplement it: state-changing operations (creating an order, updating a product) still go through classic REST endpoints with clear HTTP semantics, while SSE or WebSocket is used exclusively to notify about state changes triggered elsewhere. This separation keeps the REST API itself simple and stateless, while the real-time layer sits on top as a thin, additional communication layer.
This pattern avoids the temptation to remodel all API interaction as WebSocket messages, which in practice often leads to a less well-documented, harder-to-test API than a clear separation between state-changing REST and a notifying real-time channel.
8. Heartbeats and keepalive against silent connection drops
Both SSE and WebSocket connections can be silently closed by intermediate proxies or NAT gateways after a period of inactivity, without client or server immediately noticing, resulting in a connection that appears open to the client but is actually dead. Regular heartbeat messages (often simple comment lines for SSE, dedicated ping/pong frames for WebSocket) keep the connection active and let both sides detect a silent drop promptly.
Without a heartbeat mechanism, a client can remain mistakenly convinced for minutes that it's still receiving current updates, even though the underlying connection has long since died, which can lead to stale, incorrect displays especially in critical real-time use cases like price changes.
9. SSE and WebSockets compared side by side
The table below contrasts the key differences.
| Criterion | Server-Sent Events | WebSockets |
|---|---|---|
| Direction | Server to client only | Bidirectional |
| Protocol | Plain HTTP | Own framing protocol after upgrade |
| Reconnect | Automatic via EventSource API | Must be implemented manually |
| Proxy compatibility | Usually unproblematic | Can be blocked by restrictive proxies |
Mironsoft
OpenAPI design, Symfony APIs, and API security
APIs that external teams can integrate without back-and-forth questions?
We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.
API Review
Checking the OpenAPI spec, error formats, and status codes for consistency.
Symfony Implementation
Using DTOs, Serializer, and Validator for clean, type-safe request/response models.
Security Audit
Hardening rate limiting, auth schemes, and input validation against real attack surfaces.
10. Summary
SSE vs. WebSockets: The Essentials at a Glance
SSE for broadcasts
Simpler, more robust, and with automatic reconnect for pure server-to-client updates.
WebSocket for interaction
Necessary when both sides need to communicate as equals with low latency.
PHP-FPM limitation
Long-lived connections block FPM workers, a dedicated hub like Mercure is practically necessary for scaling.
Supplement, not replacement
Real-time channels should supplement REST for state-changing operations, not replace it entirely.