Pushing data from the server to the client is a problem almost as old as the Web.
In the early days, developers had to rely on ingenuity to achieve data push, often resulting in novel solutions like polling that compromised on performance.
However, as the demand for realtime features surged, dedicated methods to send data from the server emerged.
Today, you have four options:
- WebSockets
- XHR streaming (HTTP streaming)
- Server-Sent Events
- Server-Sent Events with Ably
Which one is best? It depends on your answer to these questions:
- Do you support resource-constrained devices like entry-level smartphones, IoT devices, or wearables?
- Will you be deploying to a network environment with stringent firewalls and proxies you can’t necessarily control? For example, an enterprise environment, school, hospital, or government institution.
- Would you prefer maintaining your client code instead of depending on a third-party library? For example, to keep the dependency chain small.
- How many concurrent connections do you need to support?
While all data push methods accomplish the same thing, they each have trade-offs in terms of compatibility, efficiency, ease of implementation, and how they scale.
This post delves into these details, exploring the benefits and limitations of each method.
By the end, you'll have everything you need to make an informed decision about which method is right for your needs.
What is data push, exactly?
Data push is a pattern where the server chooses to push data to the client(s) immediately, without having to wait for them to ask for it.
You might consider this pattern when you have some frequently updating data to display, such as a courier’s live location, a stock price update, or the status of a long-running business process.
The alternative to data push is data pull whereby, based on some user action, or after a certain amount of the time, the client requests (pulls) new data.
Although pulling and pushing both allow the user to see new data, pulling incurs much more latency due to the time necessary to initiate and route each new request.
By comparison, the data push methods we’re looking at in this post transmit data over a long-lived connection. Since the connection is always open, fresh data can be pushed to the client as soon as it’s available, minimizing latency.
Data push is much more network efficient but since each open connection consumes resources, this creates new challenges around hardware resource consumption and scaling that we’ll explore throughout this post.
When data push is the wrong choice
Data push is one-way from the server to the client and does not allow for two-way communication over the same channel.
That isn’t to say an app utilizing data push can’t also send data to the server.
It can!
But you need to use a separate channel.
For example, you can use data push to stream data from the server, while a separate HTTP channel is used to post data from the client (maybe using the Fetch API).
The downside here happens when you need near realtime updates in both directions.
This is because using a separate channel like this adds overhead.
Not a lot, but it’s enough to have a noticeable impact on performance for things like games, multiplayer collaboration, and chat, where information has to flow simultaneously and independently in both directions. In those cases, you would be better off with a bidirectional method like WebSockets.
Considerations when choosing a data push approach
You may be wondering why, in this modern day, we can’t recommend a go-to solution and be done with it? After all, “data push is almost as old as the web”!
That would be nice. But the web is rarely that tidy, and data push is no exception.
Over the years, several solutions have spawned - each of them with their own history, design goals, inherent advantages, and shortcomings.
To help you choose a method that aligns with your specific requirements, here is a list of the key questions you should consider beforehand:
- Will your clients connect from an enterprise network? Stringent firewalls or misconfigured proxies sometimes present a barrier for effective data push.
- Is keeping your client bundle small a key concern? Dependencies can be heavy, inflexible, and may require frequent updates.
- Do you support low-memory devices? Smartphones, wearables, televisions, and IoT devices demand an efficient data push implementation.
- Do you support legacy browsers? Older browsers may not support modern data push methods therefore a polyfill may be necessary.
- Are you short on developer resources? Some methods have built-in features like auto-reconnect, while others require more time and effort to implement everything from scratch.
- Does your app need to open multiple connections? Opening multiple connections from one browser can encroach on the browser connection limit, which can be as low as six.
- Is battery life a key concern for your mobile users? Keeping a connection open drains the battery quickly unless there are optimizations available.
- Will you likely need bidirectional in the future? Some apps evolve to require bidirectional, although many do not. If you think it’s likely, you may want to future-proof your choice.
- Do you need to transmit binary data from the server? Some methods support binary, while others do not.
By examining each data push method and evaluating their advantages and limitations, we will address all these questions and provide a comprehensive overview.
The importance of reliable and low latency updates
Whether you have one hundred connected users or one hundred thousand, they all expect updates in realtime, as they happen.
Although pushing data to a small number of clients is unlikely to present a challenge, things become exponentially harder the more users you serve and the more distributed they are.
And when many of these users are connected over unreliable mobile networks, things get even harder yet.
While the considerations in the previous section are essential to determine which method is best for your requirements, it’s also worth taking a moment to consider the challenges delivering realtime updates common to every data push method.
Although long-lived connections are efficient, they tie up resources on the server, resulting in some unique challenges that, left unsolved, can lead to dropped connections, missing data, and other intermittent errors that hinder your user’s experience and potentially even disrupt business operations.
Nobody wants that, so before delving into the specific methods for data push, let’s recap the three requirements for delivering successful realtime updates:
- Operating at scale: While every data push method has a different story, they all fundamentally work by opening a long-lived connection to the server. This is great for network efficiency and latency, but since each open connection consumes server resources like memory, processing power, and sockets, you may find yourself encroaching on your server limits more quickly than if you were using short-lived connections. As you near capacity, this can lead to connections being dropped, and even major issues with reliability like missed messages and total outages. That is, unless you find a way to distribute the load over multiple servers using load balancing techniques. What’s more, app usage can be unpredictable. Applications may experience sudden spikes in usage during peak hours or events, or during product launches and marketing campaigns. In those situations, the infrastructure powering realtime updates needs to be able to react to ongoing demand and shift to handle growth in concurrent users, all while maintaining low latency.
- Low latency: When your users are connecting from all around the world, you’ll need to take additional steps to guarantee messages traverse through the network as efficiently as possible – for example, by using a CDN with globally dispersed points of presence.
- Maintaining data integrity: The internet is an unreliable place. Mobile users go through tunnels, routers sometimes crash, and we all turn data off when the push notifications get too much. Apart from implementing logic to automatically reconnect and resume messages on the client, the server also needs to buffer events on its side until the client reconnects. To guarantee this data is available even in the face of a server outage, it’s essential that you immediately migrate data so you can replicate it across two or more availability zones.
Approaches to data push, compared
Now that you're aware of what to watch out for, let's delve deeper into how these data push approaches stack up, beginning with WebSockets.
WebSockets
WebSockets is a realtime technology that enables full-duplex bidirectional communication between a client (typically a web browser) and a server, supported in ninety-seven percent of browsers.
Under the hood, a continuous two-way connection is used to transmit text or binary efficiently, keeping bandwidth and latency to a minimum.
That said, just because the connection is two-way doesn’t mean the client has to send anything.
By sending data only from the server, WebSockets becomes an effective means for data push, with the added benefit that it’s just as easy to send data to the server as it is to receive data from the server.
On the surface, this is appealing. Should you need two-way in the future, you don’t have to do a rewrite. However, since WebSockets requires the most effort to implement reliably, you’ll need to consider if the benefit outweighs the implementation issues.
It’s helpful to remember that while some apps need bidirectional, many do not. Most apps out there are predominantly read apps.
Said another way - most of the time, the server sends the majority of messages while the client just listens and once in a while sends updates.
If that sounds like your app, it would be acceptable to occasionally post messages from the client using a good old fashioned async HTTP request while you benefit from a more convenient data push method from the list below.
We’ll take a closer look in a moment but first - why are WebSockets so time-consuming to implement reliably?
Challenges with WebSockets
WebSockets have one major drawback: They do not work over HTTP.
Rather, they upgrade the HTTP connection to a WebSocket connection.
As a result, WebSockets can’t benefit from any HTTP features, so you’ll eventually need to implement all these things yourself:
- Compression
- Multiplexing
- Authentication
- Security features
This dependence on a custom protocol is also one of the main reasons WebSockets are challenging to scale.
Moreover, since the WebSocket API is quite thin, many of the design decisions (and the work to implement them) are left up to you.
To give you an idea - most WebSocket implementations need a heartbeat, auto-reconnect, and a fallback in case the WebSocket connection is blocked.
Blocked?
Yep.
Stringent (or misconfigured) firewalls, proxies, or pesky enterprise middleboxes can effectively block the connection and, since you don’t control the network the client is connecting from, there might not be much you can do.
To work around this, most WebSocket implementations have a fallback to long polling that’s more work for you to implement.
In the real world, developers seldom start from scratch. Instead, they choose to use a library like ws, SockJS, or Socket.IO but that's a dependency you might not want to burden yourself with.
Thankfully, the issue with middleware isn’t as prevalent as it once was (especially if you use TLS). But it is, unfortunately, still an issue.
XHR Streaming
XHR streaming (sometimes called HTTP streaming or response streaming) is a technique whereby the server responds to a HTTP request with partial responses over time, effectively allowing the server to push text data over a continuous connection with minimum latency.
The term XHR comes from XMLHTTPRequest but the wonderful thing about this technique is that it works with any HTTP client like the more modern fetch or OKHTTP in the Android world.
Since XHR streaming is rooted in HTTP, it works on any device that can make a HTTP request.
In other words, pretty much all of them!
Although XHR is not without limitations, it is the most widely compatible approach on this list, making it an attractive option if you need to support older technology still in use due to compatibility issues with legacy systems or processes.
Compared to WebSockets, data can only flow one-way, but time and effort required to implement XHR on the server is much less.
Because XHR builds on HTTP you can benefit from HTTP features like compression and multiplexing for free.
Additionally, XHR works with your existing HTTP infrastructure, dramatically simplifying the server implementation by allowing you to benefit from your existing proxy and server-side technologies.
Implementing XHR on the client is pretty straightforward too.
Although XHR is not specifically designed for data push, it is an accepted practice to adapt XHR support for streaming downloads to implement realtime updates.
It works by manually tracking the offset for seen bytes then manually slicing the responseText attribute.
Here is an example from the book High Performance Browser Networking:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/stream');
xhr.seenBytes = 0;
xhr.onreadystatechange = function() {
if(xhr.readyState > 2) {
var newData = xhr.responseText.substr(xhr.seenBytes);
// process newData
xhr.seenBytes = xhr.responseText.length;
}
};
xhr.send();
The main downside here is memory efficiency. responseText is an unbound buffer meaning it basically holds on to data for the duration of the connection, which can lead to a memory overflow. The only way to release the buffered response is to finish the request and open a new one. For small data this may not be an issue, but for long-lived streams, especially on resource-constrained devices, this is a cumbersome problem to work around.
How does XHR fare on the network reliability and scale side?
Challenges with XHR
A XHR connection is almost guaranteed to go through since, apart from the presence of the “chunked” Transfer-Encoding header, the response is practically indistinguishable from any other HTTP request on the network.
In increasingly rare situations, a proxy may store all the chunks up before forwarding them but this is less likely to happen with HTTPS.
HTTP/1 connection limit
When making the connection over HTTP/1, Chrome limits the number of simultaneous connections per domain to six.
Since this limit is per browser, not per tab, you can imagine how you might hit this limit if you need to initiate multiple long-lived connections or support many tabs.
Introduced in 2015, HTTP/2 removed the connection limit so the question becomes: Can you use HTTP/2?
HTTP/2 is supported by ninety-seven percent of browsers and most HTTP servers added support some time ago now.
Still, the internet is an unreliable place. Servers may be limited to older versions and any forward proxy or firewall could still limit the HTTP version so you’ll need to assess your particular situation.
In summary, XHR is ubiquitous, but it is neither efficient nor the most convenient - manually slicing data is tedious compared to a built-in function, and you still need to think about how to handle disconnections and how to resume missed messages the client missed.
Today, XHR is mostly used as a fallback option in case you need to support legacy clients, which - I understand - might not sound very encouraging after reading so much about it, but don’t despair.
While XHR may not meet the whole criteria, it serves as the foundation for Server-Sent Events - a transport optimized for the streaming use case and the subject of the next section.
Server-Sent Events
Server-Sent Events (SSE) is a technology that enables servers to push text data to clients over HTTP.
To accomplish this, SSE introduces two components:
- EventSource: A cross-platform browser API supported ninety-seven percent of of browsers which allows the client to receive data from the server.
- Event stream format: An open data format, which is used to deliver the individual updates .
Under the hood, SSE provides a memory efficient cross-browser implementation of XHR streaming.
It’s the newest data push option and it comes with some appealing benefits:
- Automatic reconnect with stream resume: The EventSource interface has a built-in reconnect feature and will send the server a “Last-Event-ID” header to indicate the last event it received. You can now code the server to resume the event stream from the last known event.
- Connectionless push: According to the SSE specification, clients connected on a mobile network may offload the management of the connection to the network carrier. In such a situation, the client (probably a smartphone) can power down the antenna to conserve battery; when the server next sends an event, the carrier can use a technology similar to the one used for push notifications to convey the event to the device, which can wake up to continue reading the stream.
- Debugging interface: Since updates follow the event stream open format, browser dev tools like Chrome DevTools can interpret the shape of the stream to render a specialized interface to visualize and debug incoming data - you can see an example of that below.
As you will no doubt be expecting by now – for all the benefits afforded by SSE, there are some limitations to consider as well.
Challenges with SSE
Since SSE is based on XHR, it follows the same long-lived connection paradigm and shares some of the same challenges that come with it - in rare situations, proxies can interfere and load balancing long-lived connections can be challenging.
A note on binary support
Both SSE and XHR are limited to text data. If you want to send binary data, you’ll need to base64 encode it. This requires additional processing and incurs approximately 35% network overhead before compression.
If you are planning to send binary data, WebSockets might be a better choice.
With that said, if your goal is to send binary files, you may be better off sending the URL over SSE (or XHR), and then having the browser use good old HTTP to fetch it.
While EventSource checks many boxes, the client-side is just one half of the data push equation.
The ability to automatically share the “Last-Event-ID” with the server is certainly handy, but the server still needs to buffer events on its side until the client reconnects.
Moreover, there’s still work to do to ensure SSE can operate reliably and respond to surges in demand, all while maintaining high availability and low latency.
It’s a complex task that requires careful system design, performance monitoring, and continuous optimization to achieve near realtime updates globally.
Server-Sent Events with Ably
As a realtime PaaS, Ably distributes events to your users with minimum latency and the highest degree of reliability.
All you need to do is plug the EventSource client into the SSE adapter (a URL):
var apiKey ='xVLyHw.U_-pxQ:iPETgh3Vm16oUms5JgYXCYgPV8naidwIWwdhwBfI3zM';
var url ='https://realtime.ably.io/event-stream?channels=myChannel&v=1.2&key=' + apiKey;
var eventSource = new EventSource(url);
eventSource.onmessage = function(event) {
var message = JSON.parse(event.data);
console.log('Message: ' + message.name + ' - ' + message.data);
};
With just a few lines of code, you get all these benefits:
- Event resume: Working in tandem with the EventSource interface, Ably can read the “Last-Seen-Event-ID” and resume the stream precisely where it left off without you having to lift a finger. Since Ably replicates data in at least two independent datacenters, you benefit from 99.999999% (8x9s) message survivability even in the event of instance or data center failures.
- Load balancing: Ably can serve unlimited numbers of connections without you having to worry about load balancing.
- Global low latency: In the above snippet, when your client connects to realtime.ably.io/event-stream, Ably uses latency-based routing to connect to the closest edge acceleration point out of 307 which then forwards the traffic to Ably’s internal routing later. This ensures the shortest and most reliable path through the network.
- Elasticity: Ably can immediately react to ongoing demand and shift to handle growth in concurrent users. Then scale down again to conserve resources.
- Availability: Ably offers an average 99.999% (5x9s) uptime SLA – equivalent to 5 minutes 15 seconds of downtime per year.
- Pub/sub: Ably uses a pub/sub interface over channels.
When you use the Ably SSE adapter you get all the advantages of SSE and almost none of the downside.
We solve all the annoying data and infrastructure headaches so you don't need to, freeing you from design limitations so you can focus on solving the challenges that really matter, not the frustrating realtime edge cases you’re otherwise forced to think about and develop around.
Ordinarily, you might be concerned about vendor lock-in but the concern is less with Ably and SSE.
Since you’re building on an open standard, you could substitute Ably with your own endpoint, taking care to ensure your system is designed to guarantee the same quality of service.
Split, a feature management and experimentation platform, uses Ably and the SSE adapter to push updates to tens of millions of client apps, sending over one trillion events per month.
Since Split distributes SDKs to their customers, controlling the dependency chain to keep the bundle as small as possible is a key concern. Since EventSource is built-in to ninety-six percent of browsers, SSE was the perfect choice in this regard.
Which data push method is right for you?
The previous sections discussed the strengths and considerations of different data push methods, but how do you know which is best for you? The question is nuanced, based on the requirements of your application, decisions about customer expectations for latency, and the development resources you have available.
While you may already have a strong idea about which method is best for you, let’s recap:
If your clients connect from an enterprise network…
Stringent firewalls or troublesome proxies can block a WebSocket connection. If you anticipate clients connecting from schools, hospitals, government institutions, or any other restricted environments, a HTTP-based data push method like XHR or SSE is the more reliable choice. These methods are less likely to have trouble connecting, and not-so-prone to intermittent network problems, especially when used over HTTPS.
If you need to keep your client bundle small and eliminate the dependency chain…EventSource is built-in to 96.81% of browsers and allows you to receive server-side events without the need to write a lot of code or burden an inflexible and potentially heavy dependency that you now need to keep up-to-date and manage.
If you need to support resource-constrained devices…
XHR might not be the best option due to a possible buffer overflow. Instead, you should consider WebSockets or SSE, depending on your future requirements.
If you need to support legacy browsers…
Although SSE is widely supported, very old browsers may not support EventSource. While there are EventSource implementations for Android and iOS, there may not already be one for your technology stack. In such cases where you also want to use a HTTP-based method, XHR is a more widely-supported option.
If message guarantees are critical…
When you’re streaming data to visualize in a chart, message guarantees might not be so important - if a client disconnects, they can receive the newest data and all the messages they missed are out of date already. However, when apps rely on a sequence of messages that mutually depend upon one another, it’s critical the stream resumes where it left off. SSE has a built-in event resume, but preserving data integrity on the server is important too. When paired with Ably, you can rest assured your message is replicated in several environments.
If you need to open multiple connections from the same browsers
While WebSockets supports 255 connections, HTTP-based methods are limited to six with HTTP/1 or unlimited with HTTP/2.
If battery life is a key concern…
Data push is inherently efficient compared to polling but only SSE benefits from connectionless push.
If you need bidirectional later…
Only WebSockets offer two-way and full-duplex communication in the browser.
If you need to submit binary data…
Only WebSockets support binary, although, if you find yourself sending binary files from the server to the client, you may want to stop and think if you’d be better off uploading the file to a CDN and sending a textual link instead.
No matter which approach you choose, delivering a reliable realtime experience requires additional architectural and engineering challenges, especially when operating at scale. Oftentimes, it makes the most economic sense to work with a provider, allowing you to focus on frontend functionality rather than what’s going on with the background.
While Ably and the SSE adapter are a match made in heaven (if we do say so ourselves), Ably also works with WebSockets.
Whichever method is best for you, the Ably platform allows you to build and maintain a low-latency, scalable, fault-tolerant realtime infrastructure so you can focus on the features most important to your users. And with our average 99.999% uptime SLA, you can be sure their experience won’t be interrupted.
All the technologies presented in this list have a place, it is now up to you to maximize their potential in a scalable way.