When a GraphQL subscription is unsubscribed, what must the server actually cancel?
answer
- Cancelling delivery is not cancelling production
- The response stream only maps the source
- Release what the resolver registered
- Growth tracks churn, not concurrency
- One teardown path for every ending
basics
~20 sUnsubscribing cancels the response stream, and because that stream is only a mapping over the source event stream, the cancellation has to propagate: the source event stream and whatever produced it must be released too, along with any in-flight per-event execution.
solid answer
~50 sThe execution algorithm's unsubscribe step is short — cancel the response stream — but it is a contract, not a whole implementation. The response stream exists only as a mapping over the source event stream, so a server that stops delivering results while leaving the source live has done half the job. Cancellation must reach the source event stream returned by the root field, and through it the thing the event-stream resolver registered: a consumer on an event backbone, a poll loop, a database change feed, a timer. It must also settle whatever the last event's execution left in flight, including request-scoped state and outstanding downstream calls. The failure mode is quiet: no error is raised, nothing reconnects, and the server accumulates live upstream registrations in proportion to subscription churn rather than to concurrent subscribers.
code
pseudocode · 10 linesfunction createSourceEventStream(...):
registration = eventBackbone.consume(topic = "segments", filter = projectId)
stream = streamFrom(registration)
stream.onCancel:
registration.release() # release exactly what was acquired here
return stream
function unsubscribe(responseStream):
responseStream.cancel() # must propagate to the source stream,
# which releases the registration abovego deeper
Know that ending a subscription is an explicit step, not something that happens by itself, and that the server has work to do when a subscriber goes away rather than simply stopping sends.
Explain that the response stream is a mapping over the source event stream, so cancellation has to propagate down to the source and to whatever the event-stream resolver registered when it opened it.
Diagnose the leak from its signature: no errors, stable concurrent subscriptions, and resource growth tracking churn. Be ready to say how you would test it by cycling subscriptions and checking upstream registrations return to baseline.
Set the rule that acquisition and release live together on the source stream, and that every way a subscription can end funnels into one cancellation path, so the rarely exercised paths cannot quietly diverge from the tested one.
## Why this is more than one line of the algorithm The execution model defines an unsubscribe step whose whole content is: cancel the response stream. That looks trivial until you remember what the response stream is. It is not a source of anything — it is a *mapping* over the source event stream, producing one execution result per event. Cancelling the mapping stops results reaching the subscriber. It does not, by itself, stop events being produced. So the real question is what a correct implementation has to tear down, and interviewers who ask it are usually testing whether you have ever owned a long-lived stream in production rather than only read the algorithm. ## The chain of things that must die Work backwards from the subscriber. **The response stream.** Delivery stops. Straightforward, and the only part the algorithm names. **The source event stream.** This is the object the event-stream resolver returned at subscribe time. Cancelling the response stream must propagate here, because nothing else references it. **Whatever the event-stream resolver registered.** This is the part servers get wrong, because it is invisible to the execution engine. Opening a subscription on a translation-memory graph may have registered a filtered consumer on an event backbone, opened a change feed against a store, started a poll loop, or added an entry to an in-process registry of interested parties keyed by `projectId`. None of that is described by the schema, and none of it is released unless the source stream's cancellation is wired to release it. **In-flight per-event execution.** An event may have arrived a moment before the cancellation and be halfway through its selection set, holding request-scoped state and outstanding downstream calls. A server can legitimately let it finish and discard the result, or cancel it; what it cannot do is leave it holding resources indefinitely. **Per-subscription bookkeeping.** Rate limiters, per-subscription counters and metric registrations keyed by subscription identifier all outlive the stream if nothing removes them, and a metrics registry with unbounded cardinality is its own outage. ## What the leak looks like This is the diagnostic half of the question, and it is what makes it worth asking. Nothing errors. No subscriber complains, because the subscribers who would complain are the ones who left. Concurrent subscription count looks stable and correct. What grows is heap, upstream consumer registrations, and file or socket handles — and it grows in proportion to **churn** rather than concurrency. That signature is the tell: a page that reconnects on every navigation can produce thousands of subscribe-unsubscribe cycles an hour while never exceeding a handful of concurrent subscriptions, so a server that leaks one consumer per subscription looks idle by every count that anyone is watching. The secondary symptom is slower and stranger: orphaned consumers keep receiving events, so upstream fan-out grows, and per-event work rises against a fixed 340 ms p99 budget even though the number of real subscribers has not changed. Latency degrades on a system that appears to be under constant load. ## The teardown nobody writes a test for Worth naming explicitly, because it is where the bug usually is: the resource a subscription holds is not acquired by anything the schema describes. The schema says the root field returns a payload type. It says nothing about a consumer registration, and the execution engine cannot release something it was never told about. That is why the discipline has to be local to the event-stream resolver — the code that acquired the registration is the only code that knows it exists, so it is the only code that can attach the release to the stream it returns. ## What ends a subscription, and what it all funnels into Several things can end a subscription: the subscriber explicitly unsubscribing, the transport connection dropping, the server deciding to stop the stream, or the source event stream completing on its own. The useful design principle is that all of these should funnel into the **same** cancellation path. A server with one teardown routine for explicit unsubscribes and another for dropped connections will inevitably have the leak in the second one, because that path is harder to exercise in a test. The practical form of the answer is: make the source event stream's cancellation own every resource acquired when it was created — registered in the same place, released in the same place — rather than scattering acquisition across the event-stream resolver and release across the transport layer. Then test it by churning: open and close a few thousand subscriptions and assert that upstream registrations return to their baseline, which is the only check that catches this class of bug before production does.
- How would you prove a server does not leak on unsubscribe?Churn rather than load: open and close a few thousand subscriptions in sequence, then assert that upstream consumer registrations, live stream objects and handle counts return to their pre-test baseline. Concurrency tests miss this entirely, because the leak is per subscription created, not per subscription held.
- Should an in-flight event execution be cancelled or allowed to finish when a subscriber unsubscribes?Either is defensible provided it is bounded. Letting it finish and discarding the result is simpler and avoids half-applied downstream work; cancelling reclaims resources sooner under churn. What is not acceptable is leaving it running unbounded while holding request-scoped state, since that is the same leak arriving one execution at a time.
- Why should a dropped connection and an explicit unsubscribe share one teardown path?Because they must release exactly the same resources, and the drop path is the one nobody exercises deliberately. Two implementations mean two chances to forget the upstream registration, and the forgotten one will be in the path that only fires in production. Funnel every ending — explicit, dropped, server-initiated, source completed — into the same cancellation.
Hanging up your phone stops you hearing the caller; it does not stop them talking. Unsubscribing has to reach back and end the call at the other end too.
saying these in an interview costs you the question
- Thinks cancelling delivery stops event production
- Leaves the upstream consumer registered after unsubscribe
- Only tears down on an explicit client unsubscribe
- Expects a leak here to surface as an error
- Watches concurrent subscriptions instead of churn
- Acquires resources in one layer and releases them in another