The API space is somewhat prone to the idea of something being the “XYZ killer” – new solutions are framed as game-changers or causes of obsolescence for its competition. Recently, this has become a topic in regards to HTTP/2 and GraphQL. As HTTP/2 sees wider knowledge dissemination, some have wondered if some of its features replace the need for GraphQL. This thinking is to be expected, given that HTTP/2 has many aspects that focus on multiplexed requests and asynchronous delivery. So, the question of “Isn’t that what GraphQL does?” is a natural one.
Today, we’re going to dig into HTTP/2 and GraphQL to answer that. We’ll look at the foundational problem underlying both solutions, the symptoms they are designed to alleviate, and the ultimate outputs of those efforts. We’ll look at what multiplexing in both solutions looks like, and then see whether or not one makes the other obsolete.
The Problem with HTTP/1
Before we dive into the differences (and similarities) between HTTP/2 and GraphQL, it helps first to understand the core problem in the HTTP/1 implementation. HTTP/1 (and its precursor collection) is from an era where data transfer was relatively light compared to today. At a time where a massive request was in the hundreds of bits, rather than the megabit (and sometimes gigabit) data streams enabled by today’s standards, HTTP/1 served its purpose, delivering data over a stable connection.
The problem, however, comes with increasing packet sizes – opening a TCP connection in HTTP/1 is costly, and such requests have a high overhead. Accordingly, large requests soon became untenable over a single connection, resulting in HTTP/1.1’s attempts to serve such data through persistent connections and pipelining. This approach, combining multiple requests and responses over a single connection, worked well for a time. Still, it exposed several issues – not the least of these problems was head-of-line blocking, which allowed a slow request to block everything else in the same connection, thereby introducing the connection issue anew.
An additional problem introduced by this congestion is out-of-order delivery. This problem arises when packets are forced down different connection paths after they drop and are re-sent. This problem almost overwhelmingly is caused by congestion or weak connections between client and server – with large requests, and this can result in nearly entirely useless data outputs. While there are some solutions to this (notably multicast), these are ultimately bandaids on the more significant problem.
GraphQL’s Solutions to the HTTP/1 Problem
GraphQL’s solution to this problem was to move the request paradigm to the client and allow that client to request all resources at once. While the idea of “packaging” multiple requests into a single request might seem counterintuitive, it allowed the client to request only that which it actually wanted. In allowing clients to get every piece of data exposed by the server in a single trip, requests were packaged into a single, clean request that could be served with reduced overhead and return time.
Perhaps most importantly, this paradigm delivered a solution to the problem of over-fetching and under-fetching. In the normal interaction in HTTP/1, it was more likely that a request either did not contain enough data, or it contained a lot of “junk data” that the server believed was necessary, but that the client did not want. While this was likely small in most cases, over an entire network, this fetching miscalculation meant more duplicate requests and more client transformations, exacerbating the underlying issues with HTTP/1.
What HTTP/2 Changed
HTTP/2 has a lot of solutions on offer, not the least of which is the way it handles such requests. HTTP/2 allows for request multiplexing in a very efficient way, reducing the overall cost of such requests and responses. Additionally, by packing responses using a binary framing layer (wherein each frame is part of a stream and can be combined into the greater streamed request), such calls are reduced in overall size and made that much more efficient.
Another major gain in HTTP/2 is Server Push. Put simply, Server Push allows servers to send responses to a client before they ever request the data at all. This reduces the need for inbound requests and reduces traffic congestion, which is often the result of heavy non-specialized requests. For instance, a website loading a stylesheet is not uncommon. As such, a server might automatically serve this content using Server Push, rather than wait for the client to send a second request following the connection request itself. While this does not affect the request size per se, it does reduce congestion on the inbound channel and thus frees up resources in a significant way.
Does HTTP/2 Accomplish Similar Goals to GraphQL?
If this is all sounding a bit like HTTP/2 is designed to replace GraphQL, that’s only partially right. In truth, aspects of GraphQL are now finding themselves built into HTTP/2 and HTTP/3. The reality, however, is that these aspects are only a small part of the GraphQL offering – simply put, HTTP/2 does not replace GraphQL for a couple of significant reasons.
The real value behind GraphQL is not its ability to multiplex. The significance behind GraphQL is the shift in paradigm from server-centrism to client-centrism. In a server-centric paradigm, the server tells the client what it needs, and then the client requests more information. The ultimate problem with this approach is that nobody knows what the client wants more than the client. So the communication workflow becomes a mix of guesswork (see: Server Push’s assumptions) and unexpected edge cases (for instance, a headless browser may not want, or need, a style sheet).
In GraphQL, when a client requests data, it is requesting it for a very specific use and function, and the form it requests is meant for a very specific purpose. As such, a GraphQL request is the transformation of a general resource into a specific one per the requirements of the requestor. Using HTTP/2, you can do a lot of what GraphQL does in terms of reducing the overall roundtrip and fetching issues, but you’re still ultimately coding for a single use case.
In fact, even if you code for a multitude of use cases and push those requests per circumstantial triggering (for instance, it is possible to make a paradigm in which a mobile connection as reported by browser headers gets a mobile API response through HTTP/2), you’re still adding a layer of obfuscation from the entry point to the underlying data structure. This not only adds more congestion (as a router layer of either logic or API code is now needed), it also adds complexity.
Consider a GraphQL resource serving a website with data – you could have a headless browser, a desktop browser, a mobile browser, and a plugin, all requesting the same resource for very different reasons. Each of these use cases might need a particular type of data delivered for some particular needs. HTTP/2 can, in theory, serve these requests, but you’re just adding additional complexity, and in some cases, you may be pushing more data than is needed.
For instance, mobile may not need all the data at once and instead might just want a low-data “snapshot” of the current state, which can be expanded on demand. Desktop may not be limited at all, and might just request everything in a blast. Headless options don’t have any need for graphical content, and as such, data streams may be optimized to toss out this content, thereby reducing the size of the stream. In most HTTP/2 implementations, each of these structures would need to be explicitly created, forming either a strange microservice collection of HTTP/2 responses with a handler or a Frankenstein’s Monster of routing and response filtering.
The client should define these sorts of relationships with the server, not the other way around. There are certainly cases in which this is not true – for instance, audio streaming only changes bitrates, not delivery contents – but for more dynamic content types, this relationship is not best served by HTTP/2.
Defining the Paradigms
Ultimately, the question of whether HTTP/2 makes GraphQL obsolete is a misunderstanding of what the solutions are. In plain language, they are two vastly different paradigms, and as such, they are not 1:1 equivalences.
GraphQL is a paradigm in which the client determines what data it gets and in what format. HTTP/2 is a paradigm in which the server determines what data the client gets and in what format. While the two paradigms can be hacked around to enable a multitude of different relationships, this underlying structure does not change and represents the core concepts they were designed to deliver.
The conclusion to all of this is a simple statement – no, GraphQL is not made obsolete by HTTP/2. Saying as such is a bit like saying the car was made obsolete by the airplane – yes, both technically do the same thing (get you from point A to point B), but they both offer far more features and differences between each other that the comparison becomes sort of silly outside the specific crossover in that specific form and function.
GraphQL does a lot more than just multiplexing. Until HTTP/2 and HTTP/3 has support for client-side determination of data form and structure and client declarative data fetching (that is, the ability to request data and have it filtered and combined by the server, as opposed to the client requesting multiple endpoints), GraphQL will absolutely have a purpose. More to the point, the idea of there being an omnibus solution that replaces GraphQL wholesale is not a good one – GraphQL exists for a specific purpose, and until there is a solution that is literally everything for everyone, there should be individual, use case-specific implementations on offer.
What do you think? Do you think HTTP/2 and HTTP/3 will eventually replace GraphQL? Do you think that should happen? Let us know in the comments below.