Skip to main content

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.

CharacteristicQueryMutationSubscription
PatternRequest → ResponseRequest → ResponseOpen connection
FrequencyOne-time fetchOne-time writeContinuous delivery
DirectionClient initiatesClient initiatesServer pushes
StateStatelessStatelessStateful
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"
}
}
}
Single root field

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.

Single Root Field Rule

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-ws is the modern, recommended subprotocol, implemented by the graphql-ws library. It carries queries, mutations, and subscriptions over a single multiplexed connection.
  • graphql-ws is the older subprotocol, implemented by the now-unmaintained subscriptions-transport-ws library (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

Unsubscribe triggers
  • 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:

ScenarioBetter Alternative
Data changes rarely (< 1/min)Polling
User can tolerate delayPolling with reasonable interval
Simple read-after-writeReturn updated data from mutation
One-time data fetchQuery
High volume, low priorityPolling or batch updates

Summary

ConceptDescription
SubscriptionLong-lived operation for real-time data
EventServer-side trigger that sends data to subscribers
WebSocketCommon transport for subscriptions
FilteringArguments to receive only relevant events
LifecycleSubscribe → 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.