Class 6: Subscriptions
Understand real-time data delivery with GraphQL subscriptions.
Duration: ~15 minutes | Difficulty: Beginner | Prerequisites: Class 5
What is a Subscription?
A subscription is a long-lived operation that delivers data to the client whenever a specific event occurs on the server. Unlike queries and mutations (request-response), subscriptions maintain an open connection.
| Characteristic | Query | Mutation | Subscription |
|---|---|---|---|
| Pattern | Request → Response | Request → Response | Open connection |
| Frequency | One-time fetch | One-time write | Continuous delivery |
| Direction | Client initiates | Client initiates | Server pushes |
| State | Stateless | Stateless | Stateful |
| Intent | "Give me data" | "Change data" | "Tell me when X happens" |
Why Subscriptions?
Traditional approaches to real-time data have limitations:
Polling
Client: "Any new messages?" (every 1 second)
Server: "No"
Client: "Any new messages?"
Server: "No"
Client: "Any new messages?"
Server: "Yes, here's one"
Problems: Wasted requests, latency (up to polling interval), server load
Long Polling
The server holds the request open until data is available (or a timeout); the client receives the response and immediately issues a new request. Problems: Complex, connection overhead, not true real-time
Subscriptions
Client: "Tell me when there are new messages"
Server: (connection stays open)
... time passes ...
Server: "New message: Hello!"
Server: "New message: How are you?"
Benefits: Instant delivery, efficient, single connection
Subscription Syntax
Subscriptions look like queries but use the subscription keyword:
subscription MovieAdded {
movieAdded {
id
title
releaseYear
genre
}
}
When a new movie is created, the server pushes:
{
"data": {
"movieAdded": {
"id": "99",
"title": "Dune: Part Two",
"releaseYear": 2024,
"genre": "SCIFI"
}
}
}
The GraphQL specification (September 2025 edition, Section 5.2.4.1, "Single Root Field") requires every subscription operation to have exactly one root field, and that field must not be an introspection field. To watch multiple events you open multiple subscriptions. Each event delivered by the server is shaped around that single root field.
Subscriptions in the Schema
Define subscriptions in the Subscription type:
type Subscription {
"""
Triggered when a new movie is added to the database.
"""
movieAdded: Movie!
"""
Triggered when any movie is updated.
"""
movieUpdated: Movie!
"""
Triggered when a movie is deleted.
"""
movieDeleted: MovieDeletedPayload!
"""
Triggered when a new review is posted for a specific movie.
"""
reviewAdded(movieId: ID!): Review!
"""
Triggered when the Oscar ceremony announces an award.
"""
awardAnnounced(category: String): Award!
}
type MovieDeletedPayload {
id: ID!
title: String!
deletedAt: DateTime!
}
Subscription Arguments
Subscriptions can accept arguments to filter events:
# Subscribe to reviews for a specific movie
subscription ReviewAdded($movieId: ID!) {
reviewAdded(movieId: $movieId) {
id
rating
comment
user {
displayName
}
}
}
Variables:
{
"movieId": "42"
}
Only reviews for movie 42 will be delivered. Reviews for other movies are filtered out.
Per the GraphQL spec, a subscription operation must select exactly one root field (counting through fragments). Selecting multiple root fields in a single subscription is a validation error. The September 2025 edition also forbids @skip and @include on any selection in a subscription's root selection set, because validation has to settle the single-root-field question before runtime variable values are known. If you need to listen to several streams at once, open multiple subscription operations.
Common Filtering Patterns
type Subscription {
# Filter by ID
movieUpdated(movieId: ID): Movie!
# Filter by type/category
awardAnnounced(category: String): Award!
# Filter by user
notificationReceived(userId: ID!): Notification!
# Multiple filters
messageReceived(channelId: ID!, priority: Priority): Message!
}
Transport Protocols
Unlike queries and mutations (HTTP), subscriptions require a persistent connection:
WebSocket (Widely Used)
WebSocket subprotocols for GraphQL:
graphql-transport-wsis the modern, recommended subprotocol, implemented by thegraphql-wslibrary. It carries queries, mutations, and subscriptions over a single multiplexed connection.graphql-wsis the older subprotocol, implemented by the now-unmaintainedsubscriptions-transport-wslibrary (originally from Apollo). Apollo recommends migrating off of it.
The naming is unfortunately confusing: the deprecated library is called subscriptions-transport-ws and announces the subprotocol graphql-ws, while the modern library is called graphql-ws and announces the subprotocol graphql-transport-ws. Client and server must agree on the same subprotocol identifier during the WebSocket handshake.
Server-Sent Events (SSE)
One-directional server-to-client streaming, which exactly matches subscription semantics. It runs over ordinary HTTP, including HTTP/1.1 (the graphql-sse implementation advertises itself as HTTP/1 safe), and gains from HTTP/2 multiplexing where that is available, since HTTP/1.1 caps a browser at roughly six connections per host. It traverses proxies and CDNs more easily than WebSocket, and has its own GraphQL protocol: graphql-sse. Hot Chocolate negotiates SSE from the Accept: text/event-stream header, and GraphQL Yoga uses SSE as its default subscription transport. Apollo's GraphOS Router went a different way: it does not accept WebSocket or SSE connections from clients, and streams subscription events to them as multipart HTTP responses instead.
HTTP Streaming / Multipart
Some implementations use HTTP chunked transfer or Apollo's multipart subscriptions protocol for subscriptions. Useful when WebSocket isn't available or proxy infrastructure makes WebSocket impractical. The GraphQL-over-HTTP specification leaves subscriptions out of scope, so every transport in this section is defined outside it.
Subscription Events
Subscriptions are triggered by events on the server. The spec describes execution as a two-stage pipeline. The server first creates a source stream - the raw event stream from a pub/sub system, database trigger, or other origin. Each event from the source stream is then mapped through ExecuteSubscriptionEvent to produce a response stream of GraphQL execution results, applying the subscription's selection set to each event payload.
An execution error on a single event yields a result with errors in the response stream; the subscription continues. (The September 2025 edition renamed this class of error: what earlier editions and most libraries still call a "field error" is now an "execution error".) Only an error producing the source stream itself terminates the subscription.
Common source-stream patterns:
Database Changes
User creates movie → Trigger "movieAdded" subscription
User updates movie → Trigger "movieUpdated" subscription
User deletes movie → Trigger "movieDeleted" subscription
External Events
Payment processed → Trigger "paymentReceived" subscription
File uploaded → Trigger "uploadCompleted" subscription
API webhook → Trigger relevant subscription
Scheduled Events
Every minute → Trigger "stockPriceUpdated" subscription
Every hour → Trigger "weatherUpdated" subscription
Use Cases
Real-Time Notifications
subscription NotificationReceived {
notificationReceived {
id
type
message
createdAt
}
}
Live Chat
subscription MessageReceived($channelId: ID!) {
messageReceived(channelId: $channelId) {
id
content
sender {
name
avatar
}
sentAt
}
}
Live Updates (Dashboards, Feeds)
subscription StockPriceUpdated {
stockPriceUpdated(symbols: ["AAPL", "GOOGL"]) {
symbol
price
change
updatedAt
}
}
Collaborative Editing
subscription DocumentChanged($documentId: ID!) {
documentChanged(documentId: $documentId) {
changeType
path
value
editor {
name
}
}
}
Live Scores
subscription ScoreUpdated {
scoreUpdated(gameId: "superbowl-2024") {
homeTeam
homeScore
awayTeam
awayScore
quarter
timeRemaining
}
}
Subscription Lifecycle
- Client explicitly unsubscribes
- Client disconnects (network, tab close)
- Server terminates (error, shutdown)
- Subscription completes (if finite)
Design Considerations
1. Keep Payloads Small
# ✅ Good - minimal payload
subscription MovieUpdated {
movieUpdated {
id
title
rating
}
}
# ❌ Avoid - huge payload on every update
subscription MovieUpdatedWithDetails {
movieUpdated {
id
title
fullPlot
allReviews { ... }
allActors { ... }
allAwards { ... }
}
}
2. Filter on the Server
# ✅ Good - server filters
subscription ReviewAddedForMovie {
reviewAdded(movieId: "42") { ... }
}
# ❌ Avoid - client filters from all events
subscription AnyReviewAdded {
reviewAdded {
movieId # Client checks if movieId === "42"
...
}
}
3. Handle Reconnection
Clients should handle:
- Connection drops
- Server restarts
- Network changes
Most client libraries handle reconnection automatically.
4. Consider Scale
Subscriptions are stateful. At scale, consider:
- How many concurrent connections?
- How to distribute across servers? (pub/sub systems like Redis)
- Memory usage per subscription
When NOT to Use Subscriptions
Subscriptions aren't always the best choice:
| Scenario | Better Alternative |
|---|---|
| Data changes rarely (< 1/min) | Polling |
| User can tolerate delay | Polling with reasonable interval |
| Simple read-after-write | Return updated data from mutation |
| One-time data fetch | Query |
| High volume, low priority | Polling or batch updates |
Summary
| Concept | Description |
|---|---|
| Subscription | Long-lived operation for real-time data |
| Event | Server-side trigger that sends data to subscribers |
| WebSocket | Common transport for subscriptions |
| Filtering | Arguments to receive only relevant events |
| Lifecycle | Subscribe → Listen → Publish → Unsubscribe |
What's Next?
In the next class, we'll explore Class 7: Introspection - how GraphQL APIs describe themselves and enable powerful tooling.