How do you shape a GraphQL subscription payload so one missed event is not permanent?
answer
- which shape survives one lost message
- applying it twice must change nothing
- the next event is the repair mechanism
- some objects never send another event
- show the age, do not hide it
basics
~20 sSend the identified object's current state, or a hint the client refetches by id, rather than a delta. Then the next event overwrites whatever was missed, so a gap costs freshness for one interval instead of correctness forever.
solid answer
~50 sA subscription result is one execution of a selection set, so its shape is a schema decision with a reliability consequence. **Deltas cannot heal**: miss one and the client's value stays permanently offset, while later deltas apply cleanly to the wrong base and keep it looking plausible. **Current-state payloads** — the object's `id`, `__typename`, the displayed fields and a timestamp — are idempotent: applying one twice equals applying it once, so the next event repairs the gap automatically and the resync overlap is safe. **Hint payloads** that carry only an identifier are self-healing too, and let the refetch run as the viewer with normal per-viewer authorization, at the cost of a round trip per event. Two cases still cannot self-heal: low-frequency objects, covered by the resync query, and event-only entities such as a one-shot alarm, which need a query-backed list. Carry `lastReadingAt` so the interface can show staleness rather than hide it.
code
graphql · 17 lines# Cannot heal: one missed result offsets the client forever
type MoistureDelta {
sensorId: ID!
moistureChange: Float!
}
# Heals on the next event, and is safe to apply twice
type SensorReading {
sensor: Sensor!
}
type Sensor {
id: ID!
soilMoisture: Float!
batteryPercent: Int!
lastReadingAt: String!
}go deeper
Recall the rule of thumb: send what the value is now, not how much it changed, so a client that missed a message is corrected by the next one.
Explain idempotence concretely — applying the same payload twice must leave the same state — and why that property is what makes the reconnect overlap between events and a resync query safe.
Demonstrate the diagnosis: a plausible-looking field that is quietly wrong points at a delta payload plus a gap, and your fix is a payload shape change, not a retry mechanism.
Own the classification. Decide which parts of the graph are state-shaped and safely event-fed, and which are transition-shaped and must not depend on a socket for correctness at all.
## Two shapes of payload, one of which cannot self-repair Every subscription result is one execution of a selection set, so what a subscriber receives is whatever the schema lets it select on the subscription root field's type. That makes payload shape a design decision with a direct reliability consequence, and there are broadly three shapes: - **A delta**: `moistureChange: -3.2`. The client must apply it to a value it already holds. - **Current state**: the identified object with the fields the client cares about, as of now. - **A hint**: just enough to identify what changed, with the client refetching the object by id. A delta is the only one of the three that cannot heal. Miss a single delta and the client's value is permanently offset by exactly that amount, and — worse — nothing downstream reveals it. Later deltas apply cleanly to the wrong base and the number stays plausible. Current state and hints are both **self-healing**: the next event for that object overwrites whatever the client held, so a gap costs freshness for one inter-event interval instead of correctness forever. ## The incident A farm sensor dashboard published `moistureChange` deltas for 19 paddocks. A 27-second reconnect at 14:02 dropped one reading for sensor `sf-0417`. The tile continued to update on every later event and looked entirely alive, but read 41.6 % against a true 38.4 % for the rest of the day, and the irrigation schedule keyed off it. The audit that followed found 6 of 214 sensors carrying an inherited offset of the same origin. Nothing in the system had errored; a field simply returned stale, wrong data with full confidence. Switching the payload to current state — `id`, `soilMoisture`, `batteryPercent`, `lastReadingAt` — made the same gap cost 4 minutes of staleness and nothing more. ## Idempotence is what "self-healing" actually means State the property precisely, because interviewers listen for it: applying the same result twice must leave the client in the same place as applying it once, and applying results out of order must not make things worse than applying the newest alone. Current-state payloads have that property almost by construction, which is also what makes the resync-on-reconnect overlap safe. Deltas have neither, and retrofitting them costs you an ordering and de-duplication mechanism you now have to build and operate. ## Hints, and when they beat carrying state A hint-shaped result — `Sensor sf-0417 changed` — is self-healing too, and buys three things. The refetch runs as the viewer, so **per-viewer authorization** is evaluated on a normal query path rather than inside a fan-out. The event stays small when the object is large. And a client that does not currently display that object can ignore the hint entirely and pay nothing. The cost is a round trip per event, which is the wrong trade when events are frequent and payloads are small. The common compromise is to carry the identifier plus the handful of fields the UI actually renders, and let the client fetch the rest on demand. ## The objects that still cannot heal Self-healing has one precondition: another event arrives. Two categories break it. - **Low-frequency objects.** A sensor reporting every 4 minutes recovers quickly; one reporting twice a day does not, and the resync-on-reconnect query is what covers it. - **Event-only entities.** An irrigation alarm that fires once and is never re-sent has no next event to repair anything. If the subscription is the only path by which that alarm reaches the client, a reconnect loses it permanently. Such things belong in a query-backed, paginated list that the subscription merely nudges the client to reload. ## Make the staleness visible rather than invisible The last piece is presentational and it is the one candidates most often miss. Carry a timestamp such as `lastReadingAt` in both the subscription payload and the query result, and let the interface degrade a tile that has not been refreshed inside its expected interval. A field that says *last seen 14:02* is honest; the same field rendered as a confident current value is not. This costs one nullable field in the schema and converts an invisible correctness bug into a visible freshness signal — and none of it is specified by GraphQL, it is schema design you own.
- When is a hint-only payload the better choice?When the object is large, when events are infrequent enough that a round trip per event is affordable, or when authorization is per-viewer — the refetch then runs on a normal query path as that viewer, instead of forcing the fan-out to evaluate field-level permissions for every subscriber. A client that is not currently displaying the object can also ignore the hint entirely.
- Which objects still end up stale even with self-healing payloads?Anything that does not emit again. A sensor reporting twice a day stays stale for hours, and an event-only entity such as a one-shot alarm has no next event at all — if the socket was the only path, it is gone. The first case is covered by the resync query on reconnect; the second means the data belongs in a query-backed paginated list that the subscription only nudges.
- How would you surface staleness to the user rather than concealing it?Select a timestamp such as `lastReadingAt` in both the subscription payload and the resync query, and let the interface degrade any tile not refreshed within its expected interval. It costs one field in the schema and converts an invisible correctness bug into a visible freshness signal — none of which GraphQL specifies; it is schema design you own.
A delta is a running total whispered down a line — one missed whisper corrupts every figure after it. Current state is each person announcing the whole number, so the next announcement fixes anyone who mis-heard.
saying these in an interview costs you the question
- Publishes deltas and calls the stream reliable
- Assumes every object will emit another event
- Ignores that a delta applied twice double-counts
- Sends only a change flag with no identifier
- Renders stale values as confident current ones
- Retrofits ordering and dedupe instead of resending state