When you work on a feature that requires some sort of real time functionality, it's common to think about using websockets as a swiss knife capable of doing whatever you're being asked to do, but most of the times websockets can be overkill, to the point that it might add complexity to your tech stack making your life or someone else's (your beloved devops) even more complicated than it already is.
You'll be wondering why, well, I don't want to bore you with the technical details and I want to keep this post very concise so everyone without decent network knowledge can understand my point. In case you're still curious about the reasons I'd recommend you to read this excellent post.
So, back to my point, first let's review the most common cases for real time features we can find in a web application. Based on my experience, I can give you a short list, I know there are others, but these are the ones that I recall at this moment:
- Chat
- Page updates
- Notifications
- Financial tickers
- Real time data visualization
- Real time collaboration tools
- Multiplayer games
Now, let's break the list apart in two categories based on the type of communication they require: Unidirectional vs Bi-directional
Unidirectional
This type of communication is for when you need communication to go one way only in this case, from the server to the client. In this category we can find the following use cases:
- Page updates
- Notifications
- Financial tickers
- Real time data visualization
Bi-directional
Communication happens both ways, server and client can send messages back and forth to each other, ideal use cases of this type of communication are:
- Chat
- Real time collaboration tools
- Multiplayer games
So, In which of these cases should I use websockets, well, I'd recommend you to use them only for Bi-directional communication and when your communication needs are very close to real time. Websockets are designed for this, multiplexed communication with very low latency, everything else can survive with a little bit of delay.
OK, but what do I use for everything else?
Long polling can be considered in this case, but it's proven to be very inefficient in terms of CPU, memory and bandwidth usage, so, what do I use then?, enter Server Sent Events (SSE).
Server Sent Events
SSE are a mechanism where the client can get server pushes without having to ask for them as in a traditional client/server communication, you can think of SSE as one way pub-sub pattern where the clients subscribe to a stream, then the server can use this stream to push data to the client.
At its core SSE are just a regular HTTP request/response, the only differences are in the text/event-stream
value we pass to the Accept
/Content-Type
headers of the request/response, the browser reads this as an instruction to create a persistent HTTP connection, once this persistent connection is established, the client just needs to listen for messages coming in the stream, the server can push these messages whenever it wants and the connection is closed as soon of one of the sides decides that it needs to be closed.
SSE is designed to be efficient, since it's a long lived connection, it reduces latency by eliminating costly handshakes, its protocol is simple and text based, offers gzip compression and it has a built in retry mechanism to handle broken connections.
The client side implementation of SSE is handled by the EventSource API described as part of the HTML5 standard, most modern browsers support it, except IE and Edge (of course), but don't worry, you can still use a polyfill.
The EventSource API is quite simple, you just need to create a new EventSource
object, ie:
var evtSource = new EventSource("/updates");
with this object, you can start listening for messages:
evtSource.onmessage = function(e) {
console.log(e.data);
}
You can also listen for a specific event:
evtSource.addEventListener("foo", function(e) {
console.log(e.data);
}, false);
In case thereโs any connection error, you can handle them like this:
evtSource.onerror = function(e) {
console.log("EventSource failed.");
};
And finally, when you want to close the stream:
evtSource.close();
The server side implementation is simple as well, most web frameworks already have this implemented, I'll give you an example using the Ruby on Rails built it implementation:
class UpdatesController < ActionController::Base
include ActionController::Live
def index
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 300)
loop do
sse.write('hello world')
sleep 1
end
rescue ActionController::Live::ClientDisconnected
logger.info('SSE client disconnected')
ensure
sse.close
end
end
you see?, it's just a normal Rails controller, nothing complicated (I'm watching you websockets).
With great power comes great responsibility
Browsers have a limit for simultaneous open connections to the same server, the number is around 6 in most modern browsers, this mean you'll have one less connection available for when for example the user opens your app in a different browser window/tab, now let's say the user opens 6 tabs, then you will run out of available connections. There are a few techniques to deal with this, one of them is domain sharding, but be aware, this technique can be considered harmful as well.
One application opening so many connections simultaneously breaks a lot of the assumptions that TCP was built upon; since each connection will start a flood of data in the response, thereโs a real risk that buffers in the intervening network will overflow, causing a congestion event and retransmits.
The only feasible solution to this problem could be HTTP/2 and its multiplexing feature, and guess what, you can start using HTTP/2 today, topic that I'll tackle in a future post explaining how to implement SSE over HTTP/2 with NGINX and Ruby on Rails.
Wrapping up
Next time you see a real time feature coming in your backlog, think twice about the solution you're need to offer, there's a big chance youโll end up over engineering by using websockets, SSE is an excellent solution for most of the cases, so why don't you give it a try?
Top comments (0)