DEV Community

Cover image for @deprecate WebSockets: GraphQL Subscriptions using SSE
Jens Neuse
Jens Neuse

Posted on • Originally published at wundergraph.com

@deprecate WebSockets: GraphQL Subscriptions using SSE

WebSockets are an outdated technology deemed dead thanks to the ietf not adding HTTP/2 support. Using Server Sent Events, we could simplify the implementation of GraphQL Subscriptions, reduce frontend code and increase performance as well as flexibility when writing modern frontend applications.

Subscriptions

GraphQL subscriptions are a mechanism to stream updates from the GraphQL server to the GraphQL client. You can use subscriptions e.g to automatically update the UI once there's a new message available in a chat room. Here's an example Operation:

subscription Chat {
  messages(room: 1) {
    message {
      id
      author
      text
    }
  }
}

WebSocket transport

In order to stream messages from the server to the client, there needs to be a persisted connection between the two. The most used transport for GraphQL Subscriptions is using WebSockets based on the GraphQL over WebSockets Protocol by Apollo.

I'd like to discuss various aspects of this approach and why I think we should deprecate it in favour of technology that is a much better fit, Server-Sent Events (SSE).

The GraphQL over WebSockets Protocol

To initiate the protocol it's not enough to just open a WebSocket connection. The client needs to send a "connection init" message to which the server needs to respond with "ack". WebSockets operate over TCP so it's not obvious to me why you would need to ack receiving a message.

Once the initial message is acknowledged the client needs to send a GraphQL operation alongside the ID of the operation. Both the client and the server have to remember the ID of each individual Operation because all Operations/Subscriptions get multiplexed over a single WebSocket connection. This makes the implementation of both server and client overly complex. We will see how SSE will improve this because it takes care of the multiplexing for us.

If the client decides to unsubscribe a subscription it needs to send a "stop" message over the WebSocket connection. This gets once again acknowledged by the server sending a "done" message. Again, as this is still using TCP I'm not sure why this is required.

Multiplexing over WebSockets

Multiplexing in terms of GraphQL means that multiple Subscriptions use the same connection. In case of the implementation over WebSockets multiplexing is implemented using Javascript. This code needs to be written and maintained. It needs to be transpiled and then must run in each and every browser. Ideally, we can remove as much code as possible to simplify development and keep the Javascript running in the browser as little as possible.

Multiplexing over EventSource/SSE

The Browser API to use Server-Sent Events is named EventSource. While WebSockets allow bidirectional communication, Server-Sent Events, as the name indicates only allow the server to stream events to the client and not in both directions. Luckily, we don't have to stream messages from the client to the server to implement GraphQL subscriptions. We simply open one EventSource for each Subscription. This simplifies both the client as well as the server implementation. There's no more custom code required to remember operations and their ID's to associate a message with the right Subscription. Multiplexing is done by the client & server automatically.

HTTP/2

The ietf decided to not add WebSocket support over HTTP/2. This means that for each WebSocket connection, the browser has to open a new TCP connection to the server because the initial Handshake is based on HTTP/1.1. Depending on the browser your users are using the number of allowed TCP connections per host varies between 2 and 8 averaging at around 5. Keep in mind that a user might open multiple tabs with the same website so you should try keeping open connections at a minimum.

With Server-Sent Events this is a different story. Server-Sent Events, when used with HTTP/2 multiplex automatically over a single TCP Connection. This means you can open more than 100 EventSources to the same host across multiple tabs and would still only use one single TCP connection.

Component-Based User Interfaces

These days, component-based single page applications get more and more popular. Frameworks like React, Vue, Flutter etc. all have some concept of Components that take data and render it.

Using a persisted WebSocket connection means we have to use dependency injection to make this connection available to all components.

With the EventSource API on the other hand we can simplify the code required to implement Subscriptions. Here's an example using React's Hooks API:

const SubscriptionComponent = () => {
    const [data,setData] = useState();
    useEffect(() => {
        // get's called only once when SubscriptionComponent is mounted
        const source = new EventSource("https://example.com/persisted/FooSubscription");
        source.onmessage = e => {
            setData(e.data);
        }
        return () => {
            // get's called when SubscriptionComponent is unmounted
            source.close();
        }
    },[])
    return (
        // re-renders once data updates
        <div>{data}</div>
    )
}

The implementation is rather simple. Once the component get's mounted we start the EventSource. New messages get pushed into setState() to trigger re-rendering the component. The return () => function at the end of useEffect can be used to clean up the EventSource to avoid memory leaks. No additional library is required to implement multiplexing.

Server-Side Implementation

What looks simple on the client is even simpler on the server. In comparison to WebSockets, Server-Sent Events can be implemented without any additional dependencies. Have a look at this Example written in Golang to see how simple the implementation is.

Caveats

With all the positive aspects what might hold you back from adopting this?

Internet Explorer

First of all, the EventSource API is supported by 93.8% by all Browsers. The WebSocket API, on the other hand, is supported by 97.41%. That is because IE does not support the SSE API. Is this a blocker? No, but it contradicts with the idea of reducing complexity. If you want to support IE you have to add a polyfill for the missing API.

HTTP/2

The EventSource API makes a lot of sense when used together with HTTP/2 because only then you can multiplex all Subscriptions over a single TCP connection. So, if your server or clients don't support HTTP/2 you're in trouble. If you're not sure if your clients can use HTTP/2 you might want to stick with WebSockets.

URL length limitations

The non-polyfill Browser API to initiate an EventSource doesn't allow you to send a payload. That is, you have to send the Operation as well as the variables in the query string of the URL. Due to URL length limitations, you might not be able to send the Operation in its original format. This problem is best addressed using persisted queries. I've written another article on persisted queries if you want to get more details about the concept.

Summary

Server-Sent Events and the EventSource Browser API simplify both the client as well as the server implementation a lot. We can reduce the amount of code and are able to improve performance with HTTP/2 while reducing resource consumption because we're running less Javascript code.

At the same time, we're forced to use Persisted Queries and need to migrate from our WebSocket implementation to SSE, don't we? Not really, this is where WunderGraph comes into the picture.

WunderGraph

We have done the migration already. WunderGraph takes your GraphQL Server with the WebSocket implementation and turns all Subscriptions into Server-Sent Events on the fly.

We also take care of persisting all GraphQL Operations for you and generate a client for any frontend framework you'd like to use.

This means you get all the benefits of SSE without any additional work to be done.

As a side effect your GraphQL server becomes a lot more secure because it's not directly exposed anymore to the public. It's hidden behind WunderGraph which only allows the persisted you previously registered.

Try us out!

There's one more thing..

Did you know that the Query Planner of WunderGraph allows you to join Subscriptions with other data sources?

subscription RocketStatus {
  # rocketStatus is a subscription based on polling a REST API
  rocketStatus {
    speed
    altitude
    aboveCountryCode
    # country gets joined to the Response by querying a GraphQL API using the value aboveCountryCode
    country {
      name
      capital
      currency
    }
  }
}

This feature allows you to join any type of different data sources together with very little configuration.

For the future we're planning to add support for GraphQL Federation. This means you will be able to have Subscriptions and many more features for your federated GraphQL implementations thanks to the WunderGraph Query Planner and Execution Engine.

If you're interested in this functionality, leave your email in the chat so we can inform you once we have implemented it.

Top comments (0)