GraphQL Subscriptions vs. Live Queries

GraphQL Subscriptions vs. Live Queries

There are two common ways to get live data from a GraphQL server: Subscriptions and Live Queries. A Subscription looks like this:

subscription {
  eventX {
    selection
  }
}

A Live Query looks like this:

@live
query {
  selection
}

What’s the difference between Subscriptions and Live Queries? When would you use one vs. the other? Let’s compare Subscriptions and Live Queries in theory and examples.

Similarities

Predictable Response Stream: Yes

Both are request/stream operations where the server responds to a client request with a stream of GraphQL responses, in the shape designated by the client’s request document. One caveat: some Live Query implementations send diffs instead of the entire payload.

Flexible Transport/Protocol: Yes

Both can use a variety of transports/protocol combinations.

Example transports: TCP, WebSocket, UDP, SSE, HTTP Long-polling.

Example protocols: MQTT, Socket.io, Redis, AMQP, RSocket.

We intentionally left this open in the spec because transports will inevitably vary across organizations and use cases. However, it’s unlikely that a particular combination of transport/protocol would serve only one operation and not the other.

Stateful Server & Reactive Infrastructure: Yes

For Subscription we need some sort of pubsub system. For Live Query systems that outgrow polling, we need reactive data sources (such as a database that lets you tail a query), and an accompanying programming model (such as Rx). In both cases, the server needs to store each operation execution request, subscribe to underlying source streams, and maintain an index of long-lived connections back to the client. In short, you’ll need some sort of real-time gateway. The server is also responsible for reclaiming memory/connections per client when the client disconnects.

Guaranteed, In-Order, Exactly-Once: No

Neither operation includes limitations or guarantees of nonfunctional network delivery capabilities, such as guaranteed, in-order delivery (though TCP providesthese at the transport level). For example, if you implement a GraphQL response stream over UDP, and a response payload is dropped in transit, whether you choose to resend the dropped payload is independent of the operation type.

Buffering, Throttling: No

Any long-lived connection is susceptible to disruptions. For use-cases like chat history, it’s important to re-synchronize state between client and server after disconnection. Sometimes this can be achieved by detecting the disconnection on the server, buffering payloads during, and replaying them when the client comes back online. Other times, the server may flood the client with too much data; the client and server may agree to some sort of flow control protocol.

Both of these common features can be supported or ignored independently of the operation type, and even within operation types. For example, say you have two subscriptions:

subscription a { 
  superImportantMessage {
    message
  }
}
subscription b { 
  poke {
    poke
  }
}

The server might support offline buffering for one of these subscriptions but not the other. That’s perfectly valid, and not something we should be locking down in the spec.

Differences

Live "On Demand"

You can turn a normal query into a live query by adding the designated live directive. Conversely, you can ignore the live directive and treat a live query like a normal query. For example, if the WebSocket server is unavailable, you can resort to polling the query via HTTP. In contrast, a Subscription operation cannot be polled and cannot switch between real-time and request/response modes. Personally, I think this is the superpower of Live Queries.

Specification

Subscriptions are GraphQL operations, defined in the specification. Live Queries are not formally defined in the specification, and are generally modeled by adding a special directive to a query operation. The particular directive is left up to the user.

Declaration and Client Contract

Subscriptions observe events, Live Queries observe data. Clients can indicate a subscription operation by using the subscription operation keyword:

subscription {
  eventX {
    selection
  }
}

When we execute the operation above, we’re telling the server, “whenever eventX occurs, execute selection and send me the result”.

To execute a live query, clients need to include a directive, such as “@live”, pre-negotiated between the client and server. For example:

@live
query {
  selection
}

When we execute the operation above, we’re telling the server, “evaluate [selection] immediately, and then send me a new payload whenever [selection] would yield a different response”. In other words, a Live Query response stream should resemble infinitely fast and cheap polling of standing query, while discarding duplicate responses.

Maturity at Scale

Facebook developed GraphQL Subscriptions internally starting in 2015 and used it to power global-scale features like live comments and streaming reactions. Live Queries support came later, and, to my knowledge, has never reached the same scale as Subscriptions. Across mature organizations outside of Facebook, for example, AppSync, Apollo, and Prisma, there also seems to be more experience operating Subscriptions at scale than Live Queries. Of course, this may change over time.

Service Support

There are lots of libraries and services that support GraphQL Subscriptions, such as the aforementioned AWS AppSync. Hasura supports both Subscriptions and Live Queries, and Apollo client can approximate the behavior of a Live Query with its built-in polling feature.

I hope this has served as a useful summary of the similarities and differences between Live Queries and Subscriptions. Are you using Live Queries in production? Would you like to? Should we add Live Queries to the spec? If so, what should we say about them?