Open a live cricket score app during a match and watch the total climb the instant a boundary is hit — no refresh, no waiting, no pulling down the screen to reload. Now think about placing an order on a food delivery app: somewhere, a brand-new record has to be created and saved, not just fetched. Neither of these is something a plain GraphQL query can do. A query is built for exactly one job: reading data that already exists, in a shape the client asks for. It cannot create a new order, and it has no way to tell your phone "something changed" unless your phone asks first. GraphQL gives you two more operation types to cover the rest of what a real application needs to do: mutations, for changing data on the server, and subscriptions, for letting the server push updates to you the instant they happen. This chapter builds both, carefully, using one running example — a live cricket-scoring system, the kind of engine that sits behind score-update apps you have probably used yourself.
A One-Line Reminder of What a Query Does
In a GraphQL query, the client sends a request describing exactly which fields it wants, the server walks its schema to resolve each field, and it sends back one JSON object shaped to match the request. A query like { match(id: "M-4521") { totalScore wickets } } is a single round trip: one HTTP request, one response, done. Nothing about a query changes anything on the server, and nothing about it stays open after the response arrives. Keep that "single round trip, read-only" picture in mind — mutations keep the round-trip part but break the read-only part, and subscriptions break the round-trip part entirely.
Mutations: Asking the Server to Change Something
A mutation looks almost identical to a query on the page — same curly-brace selection-set syntax, same idea of naming the fields you want back — but it starts with the keyword mutation instead of query, and by convention, the field you call is expected to have a side effect: it writes to a database, updates a record, or triggers some action on the server. Suppose the scoring system for a match has this schema:
type ScoreUpdate {
matchId: ID!
totalScore: Int!
wickets: Int!
oversCompleted: Float!
}
type Mutation {
addRun(matchId: ID!, runs: Int!): ScoreUpdate!
}
The scorer's app, sitting in the stadium, sends this operation the moment four runs are scored:
mutation {
addRun(matchId: "M-4521", runs: 4) {
totalScore
wickets
oversCompleted
}
}
Trace what happens, argument by argument. matchId: "M-4521" and runs: 4 are arguments to the addRun field — they tell the server which match and how many runs to add, exactly the way arguments to a function tell it what to compute. The server's resolver function for addRun runs, looks up match M-4521, adds 4 to its total, and returns the updated ScoreUpdate object. Because the client's selection set only asked for totalScore, wickets, and oversCompleted, that is all the server sends back, even though the ScoreUpdate type also has a matchId field:
{
"data": {
"addRun": {
"totalScore": 132,
"wickets": 3,
"oversCompleted": 15.2
}
}
}
Notice what did not change compared to a query: the client still declares exactly what shape of data it wants back, the server still resolves fields against a schema, and the whole thing is still one HTTP request with one response. What changed is that calling this field had a side effect — the score on the server is now permanently different than it was a moment ago. This is the core idea of a mutation: same request/response mechanics as a query, but the field is understood, by convention and by the schema author's design, to change state rather than merely read it.
This should feel familiar if you already think of the web in terms of HTTP methods: a GraphQL query plays the role that a GET request plays in a plain REST API — fetch, don't change. A GraphQL mutation plays the role that POST, PUT, or DELETE play — do something, then tell me the result. The difference is that in REST, "what changed" and "what you get back" are two separate concerns baked into the endpoint's URL and its fixed response format; in GraphQL, the mutation's return type has its own selection set, so the same addRun mutation can be called by a stadium display that wants the full scorecard and by a phone widget that wants only totalScore — each client asks for its own shape, even though the underlying action performed on the server is identical.
Why the Order of Mutations Matters, and Queries Don't Care
Here is a genuine and common misconception: that a GraphQL server always resolves every field of a request independently and, wherever possible, at the same time. For queries this is close to true in spirit — since query fields only read data, a server is free to resolve several top-level fields concurrently, because reading two unrelated pieces of data in either order (or at the same instant) produces the same result either way. But the GraphQL specification treats mutations differently on purpose: when a request contains more than one top-level mutation field, the server must execute them serially — one field's resolver must finish completely before the next one starts — and in the exact order the fields were written in the request.
Work through why this matters with a concrete numeric example. Suppose a schema lets you move money between two practice-wallet accounts:
mutation {
debitAccount(accountId: "A1", amount: 500) {
balance
}
creditAccount(accountId: "A2", amount: 500) {
balance
}
}
Say account A1 starts at ₹2000 and account A2 starts at ₹800. If debitAccount and creditAccount were resolved in parallel with no ordering guarantee, you would have no assurance that the ₹500 was actually removed from A1 before it was recorded as added to A2 — and if either resolver reads a stale balance while the other is mid-write, you can end up with a lost update, where the ₹500 seems to appear in A2 without ever having correctly left A1. Because the spec forces mutation fields to run one at a time, in order, you can trace the transaction with confidence: debitAccount completes first, leaving A1 at ₹1500; only then does creditAccount begin, leaving A2 at ₹1300. It is worth being precise about what this guarantee does and does not give you: GraphQL's serial execution rule prevents the GraphQL layer itself from interleaving these two field resolutions, but it does not replace a database transaction — if the server crashes between the debit and the credit, you still need proper transactional guarantees at the database level to avoid losing money. GraphQL's ordering rule is a correctness tool for the resolver layer, not a substitute for database atomicity.
Subscriptions: When the Server Has to Speak First
Queries and mutations share one deep assumption: the client always speaks first. The client sends a request; the server replies once; the connection is done. That model has no way to represent "tell me the moment the score changes," because nobody knows in advance when that will happen — the client cannot simply ask again and again fast enough without wasting enormous amounts of effort, and it cannot predict the right moment to ask even once.
A subscription breaks the request/response assumption on purpose. When a client sends a subscription operation, the server does not send back one final answer and close the connection — it keeps the connection open and sends a new response every time a matching event occurs, for as long as the client stays subscribed. Think of the difference the way you would think about the difference between sending someone a text message and joining a WhatsApp group: sending a text is one request, one reply, done — that is a query or a mutation. Joining a group is different — you do nothing further after joining, yet every time somebody posts, the message reaches you automatically, pushed to your phone without you having asked again. A GraphQL subscription is that group-membership model applied to data.
Because an ordinary HTTP request/response cycle closes as soon as the response is sent, subscriptions are almost always carried over a different kind of connection — most commonly a WebSocket. A WebSocket is a connection that, unlike a normal web request, stays open in both directions once it is established, so either side — client or server — can send a message down it at any time, without the other side having to ask first. The client opens a WebSocket, sends its subscription operation once, and then simply listens; the server sends new data down that same open connection whenever there is something new to report.
A second common misconception is worth naming directly: that a GraphQL "subscription" is just a fancy name for the client silently re-sending the same query every few seconds — polling in disguise. It is not, and the distinction matters both for correctness and for efficiency. In polling, the client always initiates every request, the server has no idea anything changed until it is asked, and the client either wastes requests when nothing happened or waits too long when something did. In a genuine subscription, the client asks exactly once, the connection stays open, and it is the server that decides when to send data, the instant it has something new. The client never re-asks.
Publish/Subscribe: The Pattern Behind Subscriptions
The mechanism a GraphQL server typically uses to implement this push behaviour is a well-known design pattern called publish/subscribe, or pub/sub. The idea, stripped down: there are named channels (sometimes called topics or triggers); anyone can publish an event onto a channel; and anyone who has subscribed to that channel receives a copy of every event published to it, automatically, the moment it is published. A school PA system works this way — the office publishes an announcement once, over one channel, and every classroom subscribed to that PA speaker hears it at the same moment, without the office needing to know or care how many classrooms are listening.
In our cricket example, the channel is score updates for one match. Extend the schema with a Subscription type:
type Subscription {
onScoreChange(matchId: ID!): ScoreUpdate!
}
A fan's app sends this operation once, and then simply waits:
subscription {
onScoreChange(matchId: "M-4521") {
totalScore
wickets
oversCompleted
}
}
On the server, a small library such as graphql-subscriptions provides a PubSub class with two matching operations: publish(channelName, payload) to broadcast an event, and a way to obtain a stream of events for one or more channel names to hand to a subscribed client. Here is the resolver code, with the mutation and the subscription wired together through the same channel:
const { PubSub } = require("graphql-subscriptions");
const pubsub = new PubSub();
const SCORE_CHANGED = "SCORE_CHANGED";
const resolvers = {
Mutation: {
addRun: (parent, { matchId, runs }) => {
const updated = applyRunsToMatch(matchId, runs);
pubsub.publish(SCORE_CHANGED, { onScoreChange: updated });
return updated;
}
},
Subscription: {
onScoreChange: {
subscribe: () => pubsub.asyncIterableIterator([SCORE_CHANGED])
}
}
};
Read this resolver code as a trace, not just as syntax. When addRun runs, it does two separate things: it updates the score (the same work it always did as a plain mutation), and it calls pubsub.publish, dropping the fresh ScoreUpdate onto the SCORE_CHANGED channel under the key onScoreChange, matching the subscription field's name so the server knows which piece of the payload to hand back. Separately, every fan who is currently subscribed has an open asyncIterableIterator listening on that same channel name; the moment an event is published, each listener receives it, the server runs the fan's selection set (totalScore, wickets, oversCompleted) against that payload, and pushes the resulting JSON down that fan's open WebSocket. One older detail worth flagging so illustrative code does not go stale: earlier versions of this library exposed this stream-fetching method as pubsub.asyncIterator(...); current versions renamed it to asyncIterableIterator to line up more precisely with JavaScript's standard AsyncIterable interface. Both names do the same job — return a stream of events for one or more channel names — so if you see asyncIterator in an older tutorial, treat it as the same idea under an earlier name.
Diagram: How One Run Reaches Every Fan
The picture below traces the exact three-step causal chain described above, for a match with three fans currently subscribed. Notice there is only one arrow leading out of the Scorer's App — into the server — because the scorer never talks to any fan directly; every fan's update is pushed by the server, and only by the server, after the mutation completes.
Worked Trace: Push versus Poll, by the Numbers
It is worth actually counting requests to see why push matters, not just take it on faith. Suppose, before subscriptions existed, the three fans' apps instead used plain polling: every 5 seconds, each app sends a fresh query asking for the current score, whether or not it has changed. A T20 match runs for roughly 3 hours including both innings. In seconds, that is 3 × 3600 = 10800 seconds. Polling every 5 seconds means 10800 ÷ 5 = 2160 requests sent by a single fan's app over the whole match. With three fans doing this independently, that is 2160 × 3 = 6480 requests sent in total — the overwhelming majority of which arrive at moments when nothing had actually changed since the last poll.
Now count the push version. Assume, generously, that the server publishes one score-update event for every ball bowled — a reasonable upper bound, since not every ball changes the total, but let's not undercount. A T20 innings is 20 overs, which is up to 120 balls; two innings gives roughly 240 balls across the match (real matches have a few extra balls from wides and no-balls, so treat this as an estimate, not an exact count). Each of those 240 events is published once by the server and pushed once to each of the three subscribed fans, giving 240 × 3 = 720 push messages sent in total over the whole match. Compare 720 to 6480: pushing sends roughly nine times fewer messages than polling every 5 seconds, and it does so while also being faster — a push arrives within milliseconds of the event happening, where a 5-second poll can lag an actual score change by up to 5 full seconds before the next poll happens to catch it.
Query, Mutation, and Subscription, Side by Side
- Query — client initiates; one request, one response; read-only; a server may resolve multiple top-level fields concurrently since order does not affect a read's correctness.
- Mutation — client initiates; one request, one response; has side effects; the specification requires top-level mutation fields to execute serially, in the order written, so side effects cannot interleave unpredictably.
- Subscription — client initiates once, then the connection stays open; typically carried over a WebSocket rather than a single HTTP request/response; the server pushes a new response every time a matching event is published, for as long as the client remains subscribed; the client never has to ask again.
Test Yourself
Work through these using the cricket-scoring schema from this chapter — try to answer before checking the notes underneath each one.
- 1. A request contains two mutation fields:
addRun(matchId: "M-4521", runs: 6)followed byaddWicket(matchId: "M-4521"). Which one's resolver is guaranteed to finish first, and why does the GraphQL specification enforce this rather than leaving it up to the server? - 2. A classmate says: "A subscription is just a query that the client happens to send every 2 seconds instead of every 5." Identify exactly what is wrong with that statement, in terms of who initiates each message and how many times.
- 3. In the resolver code shown, the mutation calls
pubsub.publish(SCORE_CHANGED, { onScoreChange: updated }). Why does the payload object use the keyonScoreChangespecifically, rather than some other name likedataorresult? - 4. If a fourth fan subscribes to
onScoreChange(matchId: "M-4521")halfway through the match, does the server need to change anything about how the mutation resolver publishes events? Explain in terms of the pub/sub pattern. - 5. Suppose 10 fans are subscribed instead of 3, and the match still produces roughly 240 score-changing balls. Recompute the total number of push messages sent over the whole match.
Notes: (1) addRun finishes first because it is listed first, and the spec forces top-level mutation fields to run one at a time in document order — this exists so that side effects (writes) never interleave in an unpredictable way, unlike reads, whose order of execution cannot make a read wrong. (2) The core error is who initiates: in polling the client sends a new request every interval regardless of whether anything changed; in a real subscription the client sends exactly one operation and the server decides when to send data, only when there is actually something new. (3) The key must match the subscription field's name, onScoreChange, because the server's default resolver for a subscription field looks up that property on the published payload to build the response — a mismatched key would mean the field resolves to nothing. (4) No — this is the entire point of pub/sub: the mutation resolver does not know or care how many listeners exist on a channel; it publishes once, and every currently-subscribed listener, however many there are, receives its own copy automatically. (5) 240 × 10 = 2400 push messages.
Summary
A GraphQL query reads data in one request/response round trip. A mutation uses the same selection-set syntax but is understood to change state on the server, and the specification guarantees that when a request names several top-level mutation fields, they execute one after another, in order — never in an unpredictable interleaving — which is precisely the guarantee a query does not need and does not get. A subscription abandons the single round trip altogether: the client sends one operation over a connection that stays open, typically a WebSocket, and the server pushes a fresh response every time a relevant event occurs, using the publish/subscribe pattern — a resolver (often triggered by a mutation) publishes an event onto a named channel, and every client currently subscribed to that channel receives its own pushed copy, shaped by its own selection set, without ever having to ask again.