Our journey from WebSockets to HTTP/2

A story of what we learned while migrating

Miguel Jiménez
Building Lang.ai

--

HTTP/2: Hype is on

The new HTTP protocol version landed with the promise to fix a bunch of HTTP/1.1 issues — a lot of cool features, all aimed to increase performance, perceived rendering time and providing an exit away from all those front-end optimization hacks out there like resource inlining, domain sharding…

In particular, we were interested in multiplexing. Our dashboard is made of a lot of components, each one issuing one or more requests at the same time; multiplexing makes it possible for several requests to coexist on a single TCP connection, thus reducing resources usage and response times.

So we decided to give it a try for our client’s dashboard here at Séntisis. We are currently using Node.js and WebSockets(socket.io). This was my impression when I first faced the problem:

Migration in my head

But then you notice that HTTP/2 does not support WebSockets (ouch). Still, not that big of a deal, since we realized WebSockets is overkill for us (more on that later) and the plan is to move to a more RESTful API over pure HTTP.

Push

The problem then shifted to a different issue... One of the things we didn’t want to lose in the process was the ability for the server to send data to the client without the client actually asking for it. This may come in handy in these situations:

  • Asking for a long-lasting process (generating a report for example) and waiting for it.
  • Real-time updates (news, monitoring tool…)
  • Chat application
  • Multiplayer online gaming

The first two examples are the ones applying for us. So the question now turned into:

How do we keep this Push feature on HTTP/2 (without WebSockets)?

The answer was seemingly easy — we have the marvelous Push on HTTP/2! Piece of cake. And since WebSockets and HTTP/2 cannot live together, we just have to keep a double game on the server while the migration is on:

Migration in my head (v2)

Kind of cumbersome but no too hard… Done, right?

Not quite, there is just only one problem… HTTP/2 Push is not what we needed.

But let’s back up a little and review some of the Push technologies out there in order to make sense of the problems and available solutions. The following list of technologies is not intended to be exhaustive, there are other options out there and other ways to solve these problems like polling, long-polling… But these are the last contenders to enter the game and may be arguably the most used ones (at least on Node.js).

WebSockets

By far the most popular and adopted technology. Easy to use (especially on Node.js) as it has cool features including bi-directional communication.

Server implementation of WebSockets
Client implementation of WebSockets

Pros

  • Well documented, widely spread
  • Good performance and resource optimization
  • Bi-directional communication
  • Broadcast capabilities
  • One size fits all solution — it covers a wide range of problems (live streams, multi-client communication…) on one shot

Cons

  • Not standard HTTP
  • Overusing it may lead to bad practices
  • May be overkill for your application

A word on these last points. At Séntisis we are using WebSockets for our dashboard which is a rather-usual web app; the decision was based on the fact we would need real-time updates, but the fact is we don’t need WebSockets for that — not even for the real-time stuff as we will see.

The lack of structure and semantics of an Event (the central concept of WebSockets) leads you naturally to drop the structure on your API — you end up throwing more and more stuff into that event, which keeps on getting fat, and suddenly your endpoints lose the notion of single responsibility (not to mention the notion of “resource” itself).

HTTP/2 Push

Server Push allows you to send site assets to the user before they’ve even asked for them

For example, if you request index.html, the server may decide that you are going to need style.css too, so it sends the file right away issuing a Push, saving round trips to the server. This Smashing Magazine article explains the motivation, implementation and performance of HTTP/2 Push on high detail.

As good as HTTP/2 features are, it’s not exactly what we were looking for. It turns out Pushes are only processed by the browser and do not bubble up to your client code. Server Push’s motivations are just different and they aim to solve other (also important) problems.

  • Suitable for resources, not features
  • Conversation starts on the client, server just listen and reacts to certain requests
  • Updates are kept on the browser cache — they do not reach your application code
  • It speeds up the flow and provides a way out of some historical dirty little hacks (inlining…)

Server Sent Events (SSE)

The idea of Server Sent Events is to provide a standard way for you to open a connection and push data unilaterally from the server to the client. Let’s check an example:

Server implementation of SSE
Client implementation of SSE

There are of course a bunch of libraries that abstract the internals of the protocol, but I wanted this example to be deliberately low-level so that we can check how easy it is and how close to HTTP is tied up. Highlights:

  • Notice how you turn everything to SSE on the server just by setting the header ‘Content-Type: text/event-stream’; this is pure HTTP. And again, on the client code, you are using the EventSource directly since it is already there for you — no libraries needed.
  • SSE uses a text-based format called event stream but you can of course work with JSON.
  • The browser will try to reconnect automatically in case the connection is lost — no client code needed for that.
  • This protocol basically works for a single client; that means you don’t have the broadcast option for free. But again, you can build broadcast capabilities on top of it, and there are libraries out there that already support it.
  • This is not a bi-directional communication system, the server unilaterally pushes data to the client.
  • Connection is kept open and listens to new updates — neat stuff for real-time systems
  • And there is more extra stuff (not in this example): here we are just picking every message that arrives, but you can send different events by specifying a name tag. You can also identify each pushed message by filling the id tag.
Chrome interprets text/event-stream and prints nicely

Pros

  • Standard HTTP, no add-ons needed
  • Good performance
  • Easy to extend (broadcast…)
  • Automatic reconnection
  • Built-in Event names and ids support

Cons

  • Unilateral communication
  • Not supported on all browsers (IE) but you can use a polyfill

Coming back to the original problem

So it turned out the solution has always been there. SSEs are a good fit for us, they allow us to move forward while keeping a healthy migration flow:

As a plus, SSE can also benefit from HTTP/2 inherent strengths and improve its performance even more. In particular, multiplexing will allow bidirectional client/server communication since requests from client and SSE events could live together in the same TCP connection.

We still need to integrate it and check its limitations, but it feels right and easy so far.

What if I still want to use WebSockets?

You can either hold your breath and wait for WebSockets support on HTTP/2 or just stick with them under HTTP/1.1. After all, we overcame almost all the problems HTTP/2 solves by pulling (clever) hacks over the years. Plus, most of the HTTP/2 features are targeted to improve the resource loading of your web, so you can greatly benefit from them if your resources and logic are separated.

Just make sure you really need them.

Conclusions

  • WebSockets are great, but abusing them may lower the quality of your code and architecture.
  • There are other ways to get the work done with simpler tools such as SSE.
  • HTTP/2 Push works at a browser level, not application level.
  • HTTP/2 does not provide a replacement for other push technologies such as WebSockets or SSE.

The next hype: GraphQL

GraphQL + Apollo is the new stack we want to try. Some of GraphQL’s features like subscription looks promising and may help us with our real-time needs.

Since GraphQL is just a specification, it does not enforce any implementation details and (interestingly enough) subscriptions can be implemented both with SSE and WebSockets.

Worth reading?

Then it might be worth recommending. Give some claps to spread it! 👏

Check the other articles in our Building Lang.ai publication. We write about Machine Learning, Software Development, and our Company Culture.

--

--

Culture lover, lively programmer, interested on how stuff works, curious about human evolution, history and future