Why we use SSE

WunderGraph provides GraphQL subscriptions via SSE (Server-Sent Events) or Fetch (as a fallback). This post explains why we decided to take this approach and believe it is better than using WebSockets.

What is a GraphQL Subscription?

GraphQL subscriptions allow the client to subscribe to changes. Instead of polling for changes, the client can receive real-time updates.
Here's a simple example from our GraphQL Federation demo:

subscription {
    updatedPrice: federated_updatedPrice {
        upc
        name
        price
        reviews {
            id
            body
            author {
                id
                name
            }
        }
    }
}

This is based on Apollo Federation. Once the “product” microservice has a price update, WunderGraph combines the data with reviews from the “review” microservice, does an additional join with some user information from the “user” microservice, and sends the data back to the client.

The client receives this as a data stream. This way the user interface can be updated in real time.

Traditional Ways to Implement GraphQL Subscriptions

The most widely accepted way to implement GraphQL subscriptions is using WebSockets. The WebSocket API is an HTTP 1.1 standard that is generally supported by all modern browsers. (According to caniuse.com, 97.02% of all browsers support the WebSockets API)

First, the client sends an HTTP Upgrade request, asking the server to upgrade the connection to a WebSocket. Once the server refreshes the connection, both the client and server can send and receive data by passing messages over WebSocket.

Now let's discuss problems with WebSockets.

The WebSocket API is an HTTP 1.1 standard

Nowadays, most websites use HTTP/2 or even HTTP/3 to speed up the website. HTTP/2 allows you to multiplex multiple requests over a single TCP connection. This means that the client can send multiple requests at the same time. HTTP/3 improves this process even further, but that's not the point of this post.

The problem is that if your site uses both HTTP/1.1 and HTTP/2, the client will have to open multiple TCP connections to the server.

Clients can easily multiplex up to 100 HTTP/2 requests over a single TCP connection, whereas with WebSockets you are forced to open a new TCP connection for each WebSocket.

If a user opens multiple tabs on your site, each tab will open a new TCP connection to the server. Using HTTP/2, multiple tabs can share the same TCP connection.

So the first problem with WebSockets is that it uses an outdated and unsupported protocol that causes extra TCP connections.

WebSockets are stateful

Another problem with WebSockets is that the client and server must keep track of the state of the connection. If we look at the principles of REST, one of them states that requests should be stateless.

Stateless in this context means that each request must contain all the necessary information to process it.

Let's look at a few scenarios for using GraphQL subscriptions with WebSockets:

  1. Send authorization header with update request

As we learned above, every WebSocket connection begins with an HTTP Upgrade request. What if we send an authorization header with an update request? This is possible, but it also means that when we “subscribe” with a WebSocket message, that “subscription” is no longer stateless, since it depends on the authorization header we sent earlier.

What if the user logged out in the meantime, but we forgot to close the WebSocket connection?

Another problem with this approach is that the WebSocket browser API does not allow us to set headers on the update request. This is only possible using custom WebSocket clients.

So, actually, this way of implementing GraphQL subscriptions is not very practical.

  1. Send authentication token with message “connection_init” WebSocket

Another approach is to send the authentication token with the “connection_init” WebSocket message. That's what Reddit does. If you go to reddit.comopen Chrome DevTools, click on the network tab and filter by “ws”, you will see a WebSocket connection where the client is sending a Bearer token with the message “connection_init”.

This approach also preserves state. You can copy this token and use any other WebSocket client for GraphQL subscription. You can then log out of the website without closing the WebSocket connection.

Subsequent subscription messages will also depend on the context that was established by the initial “connection_init” message to highlight the fact that it is still stateful.

However, there is a much bigger problem with this approach. As you saw, the client sent the Bearer token with the message “connection_init”. This means that at some point in time the client had access to this token.

This way, JavaScript running in the browser has access to the token. We've had a lot of issues in the past where widely used npm packages were contaminated with malicious code. Giving the JavaScript portion of your web application access to the Bearer token can lead to security issues.

The best solution is to always store such tokens in a safe place, we will come back to this later.

  1. Send authentication token with “subscribe” message to WebSocket

Another approach is to send an authentication token with a “subscribe” WebSocket message. This will again make our GraphQL subscription stateless, since all the information to process the request is contained in the “subscribe” message.

However, this approach creates a number of other problems.

First, it would mean that we would have to allow clients to open WebSocket connections anonymously without checking who they are. Since we want to persist our GraphQL subscription stateless, the first time we send an authorization token will be when we send the “subscribe” message.

What happens if millions of clients open WebSocket connections to your GraphQL server without ever sending a “subscribe” message? Updating WebSocket connections can be quite expensive, and you will also have to have CPU and memory to maintain the connections. When should you disable a “malicious” WebSocket connection? What if you have false positives?

Another problem with this approach is that you are more or less reinventing HTTP via WebSockets. If you send “authorization metadata” with the “subscribe” message, you are essentially re-implementing the HTTP headers. Why not just use HTTP?

We will discuss the best approach (SSE/Fetch) later.

WebSockets allow bidirectional communication

The next problem with WebSockets is that they allow bidirectional communication. Clients can send arbitrary messages to the server.

If we go back to the GraphQL specification, we see that bidirectional communication is not required to implement subscriptions. Clients subscribe once. After this, only the server sends messages to the client. If you are using a protocol (WebSockets) that allows clients to send arbitrary messages to the server, you will have to somehow limit the number of messages the client can send.

What if a malicious client sends a lot of messages to the server? Typically, the server spends CPU time and memory parsing and rejecting messages.

Wouldn't it be better to use a protocol that prevents clients from sending arbitrary messages to the server?

WebSockets are not ideal for SSR (server-side-rendering)

Another issue we've encountered is the usability of WebSockets when doing SSR (server-side-rendering).

One of the problems we recently solved is the ability to “generic render” (SSR) with GraphQL subscriptions. We were looking for a good way to display a GraphQL subscription on the server as well as in the browser.

Why do you need this? Imagine you are creating a website that must always show the latest price of a stock or article. You definitely want the website to be (almost) real-time, but you also want to display the content on the server for SEO and usability reasons.

Here is an example from our GraphQL Federation demos:

const UniversalSubscriptions = () => {
    const priceUpdate = useSubscription.PriceUpdates();
    return (
        <div>
            <h1>Price Updates</h1>
            <ul>
                {priceUpdate.map(price => (
                    <li key={price.id}>
                        {price.product} - {price.price}
                    </li>
                ))}
            </ul>
        </div>
    )
}

export default withWunderGraph(UniversalSubscriptions);

This page (NextJS) is first rendered on the server and then restored to the client, which continues the subscription.

We'll discuss this in more detail a little later, let's first focus on the issue with WebSockets.

If the server were to render this page, it would first have to start a WebSocket connection to the GraphQL subscription server. Then it will have to wait until the first message is received from the server. Only then will it be able to continue displaying the page.

While this is technically possible, there is no simple “async await” API to solve this problem, so no one actually does it as it is too expensive, not reliable, and difficult to implement.

Summary of Issues with GraphQL Subscriptions over WebSockets

  • WebSockets make your GraphQL subscriptions stateful

  • WebSockets force the browser to fall back to HTTP/1.1

  • WebSockets cause security issues by exposing authentication tokens to the client

  • WebSockets allow bidirectional communication

  • WebSockets are not ideal for SSR (server-side-rendering)

To summarize the previous section, GraphQL subscriptions over WebSockets pose several performance, security, and usability issues. If we're building tools for the modern web, we need to consider better solutions.

Why we chose SSE (Server-Sent Events)/Fetch to implement GraphQL subscriptions

Let's discuss each problem individually and see how we solved them.

Please note that the approach we have chosen is only possible if you are using the “GraphQL Operation Compiler”. By default, GraphQL clients must send all information to the server to be able to initiate a GraphQL subscription.

Thanks to our GraphQL operation compiler, we are in a unique position that allows us to send only the “Operation Name” and “Variables” to the server. This approach makes our GraphQL API much more secure by hiding it behind a JSON-RPC API. You can see an example Hereand we have also open-sourced the solution.

So why did we choose SSE (Server-Sent Events)/Fetch to implement GraphQL subscriptions?

SSE (Server-Sent Events) / Fetch do not save state

Both SSE and Fetch are stateless APIs that are very easy to use. Just make a GET request with the operation name and variables as request parameters.

Each request contains all the necessary information to initialize the subscription. When the browser communicates with the server, it can use the SSE API or fall back to the Fetch API if the browser does not support SSE.

Here is an example of a fetch request:

curl http://localhost:9991/operations/PriceUpdates

The answer looks like this:

{"data":{"updatedPrice":{"upc":"1","name":"Table","price":916,"reviews":[{"id":"1","body":"Love it!","author":{"id":"1","name":"Ada Lovelace"}},{"id":"4","body":"Prefer something else.","author":{"id":"2","name":"Alan Turing"}}]}}}

{"data":{"updatedPrice":{"upc":"1","name":"Table","price":423,"reviews":[{"id":"1","body":"Love it!","author":{"id":"1","name":"Ada Lovelace"}},{"id":"4","body":"Prefer something else.","author":{"id":"2","name":"Alan Turing"}}]}}}

This is a stream of JSON objects separated by two newlines.

Alternatively, we could also use the SSE API:

curl 'http://localhost:9991/operations/PriceUpdates?wg_sse=true'

The answer is very similar to Fetch's answer, only with the “data” prefix:

data: {"data":{"updatedPrice":{"upc":"2","name":"Couch","price":1000,"reviews":[{"id":"2","body":"Too expensive.","author":{"id":"1","name":"Ada Lovelace"}}]}}}

data: {"data":{"updatedPrice":{"upc":"1","name":"Table","price":351,"reviews":[{"id":"1","body":"Love it!","author":{"id":"1","name":"Ada Lovelace"}},{"id":"4","body":"Prefer something else.","author":{"id":"2","name":"Alan Turing"}}]}}}

SSE (Server-Sent Events) / Fetch can use HTTP/2

Both SSE and Fetch can use HTTP/2. In fact, you should avoid using SSE/Fetch for GraphQL subscriptions when HTTP/2 is not available, since using it with HTTP 1.1 will force the browser to create a lot of TCP connections, quickly exhausting the maximum number of concurrent TCP connections the browser can open by the same source.

Using SSE/Fetch with HTTP/2 means you get a modern, easy-to-use API that's also very fast. In the rare cases where you have to fall back to HTTP 1.1, you can still use SSE/Fetch.

SSE (Server-Sent Events)/Fetch can be easily secured

We have implemented a “Token Handler Pattern” to ensure the security of our API. The “Token Handler Pattern” is a way of handling authentication tokens on the server rather than on the client.

First, you redirect the user to an identity provider, such as Keycloak. Once login is completed, the user is redirected back to the “WunderGraph Server” with an authentication code. This authentication code is then exchanged for a token.

The exchange of the authentication code for the token occurs via a back channel; the browser has no way of knowing about it.

Once the code is successfully exchanged, we create a secure, encrypted, read-only cookie. This means that the contents of the cookie can only be read by the server (encrypted). Cookies cannot be accessed or modified by browser JavaScript (read-only). This cookie is only accessible from first party domains (secure), so it can only be received on api.example.com or example.combut not on foobar.com.

Once this cookie is set, every SSE/Fetch request is automatically authenticated. If the user logs out, the cookie is deleted and no further subscriptions are possible. Each subscription request always contains all the necessary information to initiate a subscription (stateless).

Unlike Reddit's approach, no authentication token is available to the browser's JavaScript code.

SSE (Server-Sent Events) / Fetch prevent the client from sending arbitrary data

Server-Sent Events (SSE), as the name suggests, is an API for sending events from a server to a client. Once initiated, the client can receive events from the server, but this channel cannot be used for feedback.

Combined with the “Token Handler Pattern” this means that we can immediately terminate requests after reading the HTTP headers.

The same applies to the Fetch API, as it is very similar to SSE.

Fetch can be easily used to implement SSR (Server-Side Rendering) for GraphQL subscriptions

The main part of our implementation of subscriptions via SSE/Fetch is the “HTTP Flusher”. After each event is written to the response buffer, we must “reset” the connection to send the data to the client.

To support Server-Side Rendering (SSR), we've added a very simple trick. When using the “PriceUpdates” API on the server, we add a query parameter to the URL:

curl 'http://localhost:9991/operations/PriceUpdates?wg_sse=true&wg_subscribe_once=true'

The “wg_subscribe_once” flag tells the server to send only one event to the client and then close the connection. So instead of resetting the connection and then waiting for the next event, we simply close it.

Additionally, we only send the following headers if the flag is not set:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

In the case of “wg_subscribe_once” we simply skip these headers and set the content type to “application/json”. So node-fetch can easily work with this API when doing server-side rendering.

Summary

Implementing GraphQL subscriptions via SSE/Fetch gives us a modern, easy-to-use API with great usability. It is performant, secure, and allows us to also implement SSR (Server-Side Rendering) for GraphQL subscriptions. It's so simple that you can even use it with curl.

WebSockets, on the other hand, bring many security and performance issues.

According to caniuse.com, 98.15% of all browsers support HTTP/2. 97.69% of all browsers support the EventSource API (SSE). 97.56% support Fetch.

I think it's time to move from WebSocket to SSE/Fetch for GraphQL subscriptions. If you want some inspiration, here's a demo you can run locally: https://github.com/wundergraph/wundergraph-demo

We have also open sourced our implementation.

What do you think about this approach? How do you implement GraphQL subscriptions yourself? Join us on Discord and share your thoughts!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *