GraphQL / Advanced: bounded and distributed execution / Lesson 15
Subscriptions, events and stream lifecycles
Separate subscription execution from event delivery guarantees and manage long-lived identity and resources.
On this page
Lesson goals
- Explain source event streams and per-event selection execution.
- Design reconnection, deduplication and authorization rechecks explicitly.
A subscription is more than a repeated query
A subscription establishes a source event stream and executes its selected response shape for each relevant event. The GraphQL specification defines language and execution semantics without mandating one transport or message broker. WebSocket and server-sent event integrations have their own protocol and lifecycle rules. A schema subscription field does not guarantee durable history, exactly-once delivery or replay after disconnection. Decide whether the feature is a best-effort live hint, a recoverable event feed or a critical workflow signal before selecting infrastructure and client behavior.
Select a bounded event payload
The subscription below selects event identity, object identity and revision so a client can reconcile its current view. The schema must define courseChanged and its event type, and the server must authorize the requested course scope. A single subscription operation must have one root field under the language rules. Keep event payloads narrow and avoid broadcasting private objects to every subscriber followed by client-side filtering. Output authorization still matters at delivery time, especially if membership or visibility changes while the connection stays open.
subscription CourseUpdates($id: ID!) {
courseChanged(id: $id) {
eventId
course { id revision title }
}
}
The fields are an application event contract. They provide reconciliation information but do not themselves implement broker persistence or replay.
Recheck long-lived authority and cleanup
Authentication at connection creation may become stale. Define token expiry, revocation, tenant changes and object eligibility during a long-lived stream. Reauthorize relevant events or terminate and reconnect under a documented policy; do not assume an initial successful handshake grants indefinite access. On completion, cancellation or disconnect, release event listeners, iterators and associated source resources. Bound active subscriptions and queued messages per principal. A slow client requires a deliberate drop, coalescing or disconnect policy rather than unlimited buffering that grows process memory.
Reconcile duplicate and out-of-order notifications
This local reducer treats a revisioned event as a hint to update a synthetic view. It ignores a duplicate event ID and refuses to replace a newer revision with an older one. A real stream may need an authoritative refetch after a gap; revisions alone do not prove that all intermediate events were delivered. The seen set is bounded by the four-event fixture here, but a production deduplication history needs an explicit retention policy. The example performs no asynchronous I/O and is not a subscription transport test.
const events = [
{id:"e1",revision:5}, {id:"e1",revision:5},
{id:"e3",revision:7}, {id:"e2",revision:6}
];
const seen = new Set();
let revision = 4;
for (const event of events) {
if (seen.has(event.id)) continue;
seen.add(event.id);
revision = Math.max(revision, event.revision);
}
console.assert(revision === 7);
console.assert(seen.size === 3);
The final revision is 7 despite a duplicate and late revision 6. This does not prove complete delivery or a gap-free event history.
Specify recovery before offering real-time guarantees
If the transport disconnects, reconnecting creates a new stream unless the application supports a documented resume mechanism. A resume cursor needs retention, scope and expiry semantics just like pagination cursors. Without reliable replay, refetch authoritative state and then reconcile incoming events under a race-aware protocol. Test disconnect, expired authority, duplicate delivery, gap detection, slow consumers and cleanup using deterministic local event sources first. Do not claim a live transport, broker or failover rehearsal from a synchronous reducer; each integration needs its own authorized evidence.
Common errors and fixes
A reconnect silently misses important changes
- Why it happens
- Live delivery was mistaken for durable replay.
- How to fix it
- Use documented replay or authoritative state reconciliation.
- Prevent it next time
- Test a disconnect spanning several source events.
An expired user keeps receiving private updates
- Why it happens
- Authorization was checked only at handshake.
- How to fix it
- Recheck eligibility or terminate under expiry policy.
- Prevent it next time
- Include revocation during active stream tests.
Memory grows with a slow consumer
- Why it happens
- Pending events had no queue bound or pressure policy.
- How to fix it
- Bound buffers and choose coalescing, drop or disconnect behavior.
- Prevent it next time
- Verify listener cleanup and queue limits in isolation.
Check your understanding
1. What does eventId solve that revision alone may not?
Show worked solution
A stable event ID recognizes an identical notification replay even when it carries the same object revision. A revision orders object state under its documented contract, but separate events can share a revision or one event can affect several records. Keep their meanings explicit. Neither field alone establishes reliable transport delivery or a complete auditable event log.
2. Why might a dashboard refetch after detecting a revision gap?
Show worked solution
A gap can mean the client lacks changes required to construct the current view. If the feed does not offer trustworthy retained replay, authoritative refetch is safer than inventing missing intermediate states. Reconcile the refetch with concurrently arriving events using the applicationโs revision protocol, and expose an appropriate temporary stale state rather than claiming continuous completeness.
Key takeaways
- Subscription syntax does not define durable transport guarantees.
- Identity, replay, backpressure and cleanup need explicit lifecycle rules.