skip to content

Several components read the same live key across navigations — how should the push subscription's lifetime be scoped?

level: seniorimportance: should knowfreq 44%

answer

  1. interest in a key, not a component
  2. first reader opens, last closes
  3. count, do not duplicate
  4. grace window survives a navigation
  5. the connection outlives every subscription

basics

~20 s

To the set of interested readers, not to one component. Reference-count per key in the cache layer: the first reader opens the subscription, later readers join it, and the last teardown closes it after a short grace window.

solid answer

~40 s

Scope it to interest in a key, held by the cache layer. Keep a count of readers per live key: the first component to read it opens the subscription, every later reader simply joins the same one, and when the count returns to zero the subscription is closed — but after a short grace window, because a navigation frequently unmounts and immediately remounts the same readers, and closing on the intervening tick means a needless close, reopen and catch-up read. The underlying connection is shared across keys and outlives all of them; only the per-key subscription is reference-counted. Getting this wrong shows up as duplicate subscriptions for one key, as a channel that dies on the first navigation, or as subscriptions that are never closed and quietly accumulate for the life of the session.

code

pseudocode · 18 lines
pseudocode
readers = {}                         // live key -> interested reader count

on a component starts reading live key K:
  readers[K] = readers[K] + 1
  cancel any pending close for K
  if no open subscription for K: open subscription for K

on that component tears down its read of K:
  readers[K] = readers[K] - 1
  if readers[K] == 0: schedule close of K after grace window

on push event for key K:
  merge event into cache entry K     // every reader re-renders from the entry

on channel reconnected:
  for each K with readers[K] > 0:
    resubscribe K
    invalidate K                     // catch-up read refills the gap

go deeper

for a junior

Remember that one live stream can feed many components. You do not open a connection inside each component that shows the data.

for a middle

Explain reference counting per key: first reader opens, later readers join, last teardown closes, and the subscription lives in the cache layer rather than in a view.

for a senior

Show the operational details: a grace window so navigation does not thrash, idempotent teardown, counts that cannot leak, and a registry of active keys that the reconnect path reuses.

for a principal

Weigh the cost per subscription against reconnect cost when setting the grace window, and decide how many live keys the client may hold at once before the channel becomes the app's scaling limit.

## The lifetime that matters is the readers', not a component's The instinct is to open a subscription where the data is used and close it when that component goes away. It fails on both ends. Several components on one screen often read the same key, so per-component subscriptions multiply the work the server does. And a single component's life is much shorter than the data's relevance: a navigation that unmounts and remounts the screen would close and reopen the channel for no reason, losing every event in between. The correct unit is **interest in a key**, tracked where the cache lives. ## Reference counting 1. A component starts reading a live key. The count for that key goes from zero to one, so the bridge opens a subscription. 2. More components read the same key. The count rises; nothing else happens — they all read one entry that one subscription feeds. 3. A reader tears down. The count falls. 4. The count reaches zero. A close is **scheduled**, not executed. 5. If a reader appears again inside the grace window, the pending close is cancelled and the existing subscription is reused. Otherwise it closes. The grace window is what makes navigation cheap. Its length is a judgment call, usually a few seconds: long enough to cover a route transition, short enough that a user who leaves a screen stops paying for it. ## Scoping options compared | Scope | Subscriptions per key | Behaviour across a navigation | Typical failure | |---|---|---|---| | Per component instance | one per reader | closed and reopened every time | duplicate load on the server, events lost between unmount and remount | | Per screen or route | one per screen | closed on leaving | two screens showing one record each subscribe separately | | Reference-counted per key in the cache | exactly one | survives a remount inside the grace window | needs disciplined teardown or the count leaks | | One global subscription for everything at startup | one, always | never closes | the client receives data for keys nothing displays, and the server pushes to idle clients | ## What the cache layer owns - **The connection itself**, shared by all keys and re-established on its own terms. Individual subscriptions are multiplexed over it; one screen leaving should never take the connection down. - **The registry of active keys**, which is also exactly what the reconnect path needs in order to resubscribe and to issue a catch-up read. - **Idempotent teardown.** Unsubscribing twice must be harmless, because tearing down in a runtime that may run a cleanup more than once, or in a development mode that deliberately exercises setup and teardown twice, is normal. - **Late events.** An event can arrive after the last reader is gone. Merging it into an entry nobody reads is harmless if the entry already exists and a waste if it does not; decide, rather than discovering it as growth. ## Symptoms of getting it wrong - **Duplicate subscriptions**: the server sees N subscribers for one screen, and each event is merged N times. If merges are not idempotent — appending to a list, incrementing a counter — the data is visibly wrong, not just wasteful. - **A channel that dies on the first navigation**: the subscription was owned by a component that unmounts, so the rest of the app goes quiet without any error. - **Counts that never reach zero**: a teardown path that does not run — an early return, an error thrown during setup after the count was incremented, a reader registered twice and unregistered once. Increment only after setup can no longer fail, and unregister in the same place you registered. - **Thrash**: no grace window, so every navigation pays a close, an open and a catch-up read, and the user sees a loading state that has nothing to do with their action. ## Framework variation Frameworks differ in where the registration hook lives — a lifecycle callback that runs after the view is attached, an effect tied to the read, a store's own subscribe-count, or a compile-time-generated subscription block — and some intentionally run setup and teardown twice in development to surface exactly the leaks above. What does not differ is the rule: the count and the subscription belong to the cache layer, and the component contributes nothing but its interest.

  • What breaks if two components each open their own subscription to the same key?
    The server pushes each event twice and the bridge merges it twice. A replace or a patch is usually idempotent so nothing looks wrong, which is why this hides for months; an append to a list or an increment of a counter is not, and the duplicate shows as doubled rows or inflated totals under load.
  • How long should the grace window be?
    Long enough to cover a route transition and a remount, short enough that an abandoned screen stops costing the server — a few seconds is typical. Tune it against the cost of resubscribing: expensive catch-up reads argue for longer, high per-subscription server cost argues for shorter.
  • Why does the registry of active keys matter beyond lifetime management?
    Because it is exactly the input the reconnect path needs. On reconnect the bridge must resubscribe the keys that still have readers and invalidate them so a catch-up read refills whatever changed during the gap. Without a registry, reconnect either over-subscribes or silently restores fewer streams than were live.

saying these in an interview costs you the question

  • Opening one subscription per component instance
  • Tying the underlying connection to a single screen's lifetime
  • Closing immediately at zero readers, so navigation thrashes the channel
  • Incrementing the reader count before setup can no longer fail
  • Never closing subscriptions because nothing counts readers down
  • Assuming duplicate subscriptions are merely wasteful, never incorrect