GraphQL Live Queries vs Subscriptions: How to Choose for Realtime Apps
GraphQL live queries vs subscriptions: semantics, transports, scaling, caching, and DX compared—learn when to use each and how to combine them.
Image used for representation purposes only.
Overview
Real‑time GraphQL has two dominant patterns: subscriptions and live queries. They both push updates to clients, but their mental models, infrastructure demands, and developer ergonomics differ in important ways. This article compares them side‑by‑side so you can choose the right approach for your product and team.
The mental model
-
Subscriptions: Event streams
- You subscribe to a source of events (e.g., a new message, an order status change).
- The server pushes discrete payloads when something happens.
- You reconcile those events into your local application state.
-
Live queries: Continuously fresh state
- You issue a normal GraphQL query but mark it as “live.”
- The server monitors the data dependencies of that query and re‑executes when relevant data changes.
- The client receives the latest result (often as patches), so local state stays in sync without event reconciliation.
The key difference: subscriptions are about changes; live queries are about current truth.
What the payloads look like
- Subscriptions return event objects. They may include deltas or fully materialized entities, but semantically they’re “something happened.” You often need to merge, sort, and deduplicate on the client.
- Live queries return query results (the same shape as a regular query). The server may send full snapshots or minimal patches, but the contract is “this is what the world looks like now for this selection.”
Transports and protocols
-
WebSockets
- Common for subscriptions due to full‑duplex, low‑latency streams.
- Requires connection management (heartbeats, reconnection, multiplexing operations over one socket).
- Load balancers and serverless platforms may need sticky sessions or connection offload.
-
Server‑Sent Events (SSE)
- One‑way push over HTTP; simple and firewall‑friendly.
- Works well for both subscriptions and live queries; easier to run behind CDNs and HTTP‑aware infrastructure.
- Per‑tab connection model can increase connection counts compared to a single WebSocket.
-
Long polling/fetch intervals
- Baseline fallback for environments where persistent connections are hard.
- More common as a poor‑man’s live query than for subscriptions.
Both patterns are transport‑agnostic: the semantics don’t depend on WebSocket vs SSE, but operational characteristics do.
Server architecture differences
-
Subscriptions
- Backed by a pub/sub system (message broker, database change feed, in‑process event bus).
- Resolver maps topics to GraphQL types and fields.
- Fan‑out cost is proportional to the number of subscribers per topic.
- Good fit when the event is the product (e.g., chat messages, notifications, presence updates).
-
Live queries
- Backed by a dependency graph and invalidation engine.
- The server tracks which records/keys a query touched, and re‑executes when those keys change.
- Efficiency depends on granularity of invalidation and memoization; naive re‑execution can be CPU‑heavy.
- Great for dashboards, leaderboards, and any “always‑fresh view” where correctness is a snapshot rather than a log of events.
Consistency and correctness
-
Ordering and gaps
- Subscriptions: you must consider ordering, deduplication, race conditions, and potential missed events during reconnects.
- Live queries: you get the latest snapshot after reconnect; no need to replay an event log. You may still see transient staleness between updates depending on invalidation latency.
-
Authorization
- Subscriptions: validate on subscribe and, ideally, on every event, because permissions may change between the two.
- Live queries: authorization is applied at each re‑execution; safer by default for dynamic access rules.
-
Conflict resolution
- Subscriptions often force client‑side merging logic.
- Live queries minimize custom merge code since the server sends state, not changes.
Caching and edge delivery
-
Subscriptions
- Traditional HTTP/CDN caching doesn’t apply. You might cache event payloads inside your app or broker but not at the edge.
- Client caches must reconcile incoming events with normalized entity stores.
-
Live queries
- Since results look like normal queries, normalized caches integrate naturally.
- If updates are delivered as patches, the client cache can remain compact and coherent.
- Some platforms support partial results and incremental delivery, which can reduce bandwidth and time‑to‑first‑paint for large payloads.
Scalability trade‑offs
-
Connection scaling
- WebSockets concentrate many operations per connection, but long‑lived sockets stress connection limits; you may need dedicated gateways.
- SSE uses HTTP semantics and can ride existing infrastructure but scales with the number of tabs/devices.
-
CPU and memory
- Subscriptions: inexpensive steady CPU per idle subscriber; spikes on events with high fan‑out.
- Live queries: continuous cost from dependency tracking and periodic re‑execution; well‑designed invalidation is essential.
-
Data shape stability
- Subscriptions let you tailor minimal event payloads; efficient when only a few fields change.
- Live queries can resend large selections unless patching is used; design selections thoughtfully and consider fragment boundaries.
DX: client libraries and ergonomics
- Subscriptions are first‑class in most GraphQL clients. Expect straightforward hooks, exchanges/links, and cache integration helpers.
- Live queries vary by ecosystem. Some servers and clients support a directive like
@live; others implement live behavior through polling, SSE, or custom plugins. Expect more configuration and library choice compared to subscriptions. - Testing
- Subscriptions: use event fixtures and contract tests to assert client merge logic.
- Live queries: snapshot tests against re‑executed results; easier to reason about since the payload equals “current truth.”
Security and limits
- Rate limiting should consider both connections and operations per connection.
- For subscriptions, validate each event against current user permissions.
- For live queries, enforce selection set size limits, maximum live operations per user, and minimum update intervals to prevent re‑execution storms.
Choosing between live queries and subscriptions
Use subscriptions when:
- Your domain is inherently eventful (chat, notifications, collaborative cursors, presence).
- You need strict event ordering and low latency on individual events.
- Clients already implement sophisticated reconciliation logic (e.g., timelines, optimistic updates).
Use live queries when:
- Users consume “current view” data that should self‑refresh (dashboards, leaderboards, stock/crypto tickers, IoT panels).
- Avoiding missed updates after reconnects is important.
- You want simpler client code with fewer merge paths and less custom cache logic.
Hybrid approach:
- Many apps benefit from both—subscriptions for high‑cardinality, append‑only streams (messages), live queries for aggregate/stateful views (unread counts, conversation lists).
Implementation sketches
Below are minimal, vendor‑agnostic examples to illustrate the shape of each pattern.
Subscriptions: schema and client
Schema snippet:
# Event-driven: notify when a new message is created in a room
type Message {
id: ID!
roomId: ID!
text: String!
author: String!
createdAt: String!
}
type Subscription {
messageAdded(roomId: ID!): Message!
}
Client pseudocode (WebSocket):
import { createClient } from 'some-graphql-ws-client';
const ws = createClient({ url: 'wss://api.example.com/graphql' });
const unsubscribe = ws.subscribe(
{
query: `subscription OnMessage($roomId: ID!) {
messageAdded(roomId: $roomId) { id text author createdAt }
}`,
variables: { roomId: 'general' }
},
{
next: (data) => addMessageToList(data.messageAdded),
error: console.error,
complete: () => console.log('done')
}
);
Key points:
- The server publishes an event when a message is created.
- The client appends the new message and updates any derived UI (e.g., unread count).
Live queries: query and server notes
Query with a live directive (conventions vary by library):
# State-driven: keep the leaderboard view fresh
query LiveLeaderboard @live {
leaderboard(top: 10) {
user { id name avatar }
score
rank
}
}
Client pseudocode (SSE):
const es = new EventSource('/graphql/stream', {
withCredentials: true
});
// Send the GraphQL operation using your framework's SSE protocol conventions.
// Each SSE message contains either a full snapshot or a JSON patch.
es.onmessage = (event) => {
const payload = JSON.parse(event.data);
applyGraphQLResultOrPatchToCache(payload);
};
Server notes:
- Track which rows/records the
leaderboardresolver touches. - Invalidate on score updates for relevant users.
- Re‑execute only affected queries and deliver a minimal patch.
Performance tips
-
Shape selections for stability
- Prefer stable IDs and field ordering to improve patch efficiency.
- Break large views into fragments; update only what changes.
-
Throttle and coalesce
- Batch invalidations for live queries to avoid stampedes under write bursts.
- For subscriptions, coalesce bursty events into digest payloads when UX allows.
-
Cache wisely
- Use normalized caches keyed by entity ID to reduce paint time after updates.
- For live queries, memoize resolver results when inputs are identical.
-
Connection hygiene
- Heartbeats and automatic reconnects are essential for both WebSockets and SSE.
- Use exponential backoff; re‑authenticate on reconnect.
Observability and operations
-
Metrics to watch
- Active connections, operations per connection, and reconnection rates.
- Re‑execution latency for live queries; end‑to‑end event latency for subscriptions.
- Fan‑out size per topic (subscriptions) and invalidation fan‑out (live queries).
-
Tracing
- Annotate subscription handlers with topic keys.
- For live queries, record dependency keys per operation to explain why an update was triggered.
-
Canary strategy
- Roll out live‑query invalidation rules gradually; compare CPU and trigger counts between canary and control.
- For subscriptions, canary consumer workers to validate payload shape changes before full rollout.
Common pitfalls
- Using subscriptions for aggregate views that really need current truth; clients get bogged down merging events.
- Turning every query into a live query; you pay continuous CPU for little UX value.
- Ignoring authorization re‑checks; both patterns can leak data if permissions change mid‑session.
- Over‑fetching in live queries; send large snapshots too often. Prefer patches and smaller fragments.
Decision checklist
Answer these questions to pick a pattern:
- Do users care about each event or just the up‑to‑date view?
- Can the UI tolerate occasional snapshot jumps (live queries) or does it need per‑event animations (subscriptions)?
- Which is cheaper for your workload: continuous re‑execution (live) or spiky fan‑out on events (subscriptions)?
- What’s easier for the team: client‑side reconciliation logic (subscriptions) or server‑side invalidation (live)?
- How do transport constraints (corporate firewalls, mobile radios, serverless limits) influence WebSocket vs SSE?
Summary
- Subscriptions push event logs. They shine for chatty, append‑only domains and per‑event UX.
- Live queries push state. They excel for always‑fresh views where correctness equals “what’s true right now.”
- Most real‑time products use both. Start by mapping UX requirements to semantics, then choose transports and libraries that fit your infra. With clear mental models, you can deliver responsive, maintainable real‑time GraphQL at scale.
Related Posts
GraphQL Subscriptions: A Practical Real‑Time Data Tutorial
Build real-time apps with GraphQL Subscriptions: step-by-step server, client, auth, scaling, and testing guidance using Node, WebSockets, and graphql-ws.
GraphQL Caching Strategies on Client and Server: A Practical Playbook
A practical guide to GraphQL caching on the client and server—policies, headers, CDNs, and invalidation patterns that keep data fast and correct.
Stop the N+1 Spiral: The GraphQL DataLoader Batching Pattern, Explained
A practical guide to GraphQL DataLoader batching: fix N+1, design robust loaders, TypeScript examples, caching, observability, and production pitfalls.