| Based on my experience, SSEs add more complexity and brittleness to applications compared to WebSockets.
Having a single WebSocket connection to handle both RPCs and subscriptions is great because it binds all data flows to the lifecycle of a single socket - So if that single connection fails, it's easy to identify the disconnection and recover from it. Another major benefit of WebSockets is that you can control the exact timing of RPCs and subscriptions without having to worry about race conditions when the server expects to receive actions in a specific order. Also, SSEs add a lot of overhead because you need to authenticate each SSE channel independently (unlike with WebSockets where you only need authenticate a socket once at the beginning and then use it to subscribe to almost unlimited channels); so if you have many SSE EventSources, establishing them will waste a lot of resources since you need to pass the auth token (or session ID) to each EventSource that you open - In general, SSE authentication is tricky and forces you to rely on cookies which can be a problem in a lot of situations (e.g. on mobile if the front end is loaded the local file system as part of a WebView, your cookies will not be sent to the server due to cross-origin restrictions). Moreover, the lack of control over the lifecycle of the SSE connection makes it difficult to coordinate recovery from network or server failure/restart; a common problem happens when your server crashes and then all the SSE event sources try to reconnect immediately at the same time and this DDoSes your server again, then the reboot and crash cycle repeats indefinitely...
With WebSockets, you can control the reconnect algorithm to add exponential backoff, for example (it can be customized to your exact requirements). Finally, the statefulness of WebSocket connections can be a huge advantage; you can store data pertaining to a single active client in-memory on the server-side which can be convenient for a lot of use cases (and efficient). For example, you can attach an auth token on the back end socket (in memory) and can use it to quickly check access rights for any channel without having to make an additional database call. SSEs are not a practical abstraction IMO. It's sad that people don't realize how good the WebSocket standard is. It's good because it's simple; it's also what makes it so flexible. WebSockets cover more use cases. Subscriptions as a concept have to support too many use cases (too generic) to be implemented with something as restrictive as SSEs. |
The Server-sent-events/EventSource API _seems_ like a better fit for GraphQL subscriptions because it's one-directional traffic anyway, but in addition to the issues you mentioned above, you'll also run into the 2000-character limit most browsers have for URLs [1]. The browser's EventSource constructor does not let you specify request bodies so you're forced to put everything in the URL, which is _not_ suited for the graphql queries which tend to get pretty large. I ended up writing an alternate EventSource parser using the fetch() api [2] to get around this limitation.
Happily, there seems to be two alternate approaches in the pipeline that seem more robust/better supported:
1. The proposed @defer graphql directive [3] recommends using multi-part HTTP as the standard transport layer, so hopefully all the tooling like GraphQL Playground will support a single-request-multiple-response scenario over plain HTTP
2. For scenarios where we need interactive, bidirectional communication, the proposed WebTransport API [4] seems to solve some of the issues with websockets.
[1] https://stackoverflow.com/a/417184
[2] https://github.com/Azure/fetch-event-source
[3] https://www.apollographql.com/docs/react/v2.4/features/defer...
[4] https://github.com/WICG/web-transport