skip to content

A GraphQL subscription executes for 3,000 subscribers, then an authorization check empties most payloads. How would you cut that cost?

level: seniorimportance: should knowfreq 48%

answer

  1. The rule was right, the placement was wrong
  2. Three places a predicate can live
  3. Make the event fat enough to decide early
  4. Executions per useful delivery
  5. An empty payload is still a disclosure

basics

~20 s

Move the predicate earlier. Narrow what each node receives at the event source, then drop events per subscription using the stored variables and context before executing, so only subscribers who will actually receive a payload cost an execution.

solid answer

~50 s

There are three places a filter can sit, and their costs differ by orders of magnitude. **At the source**, a node subscribes only to the event partitions its subscribers care about, so irrelevant events never arrive. **Between the event and execution**, a predicate over the raw event plus that subscription's stored variables and context decides whether to execute at all. **Inside execution**, a resolver or authorization rule evaluates the same thing after the work is done — correct, but you have already paid. A check that only runs deep in field resolution costs a full execution per subscriber and then throws the result away, and if the empty result is still delivered it also tells the subscriber that something happened. The fix is to make the predicate computable without resolvers: put the fields it needs on the event itself, evaluate entitlement at subscribe time, and re-evaluate on a revocation event rather than per field.

code

pseudocode · 14 lines
pseudocode
# before: 3,187 executions, 62 useful deliveries
for record in subscribersOf(event.flightId):
    result = execute(record.ast, record.variables, record.context, event)
    # entitlement enforced inside a field resolver, after all the work
    if result.isEmpty(): continue
    record.sink.send(record.operationId, result)

# after: 62 executions, 62 useful deliveries
for record in subscribersOf(event.flightId):
    if event.cabin != record.variables.cabin: continue
    if event.restrictedTo and event.restrictedTo not in record.context.capabilities:
        continue                       # decided from data already in memory
    result = execute(record.ast, record.variables, record.context, event)
    record.sink.send(record.operationId, result)

go deeper

for a junior

Be ready to state the basic idea: deciding who should receive an event before running the query is cheaper than running it for everyone and throwing results away. Recall that a subscription's own arguments should filter events, not just shape output.

for a middle

Explain the three placements — at the event source, before execution, and inside execution — and what each one costs. Be able to say what the source event must carry for a predicate to be evaluated without touching a resolver.

for a senior

Show the whole diagnosis: measure executions against useful deliveries, find the late predicate, hoist it, and name what you now owe — revocation handling and a suppression rule so an empty delivery never leaks that an event happened.

for a principal

Own the boundary between the authoritative rule and its cheap approximation. Be ready to argue where each lives, who reviews changes to the event payload that the predicates depend on, and what regression would tell you the optimisation has drifted from the rule.

## The incident shape A seat-map subscription serves 3,187 watchers on one flight. Corporate-fare inventory is visible only to accredited agents, of whom there are 62. The entitlement was enforced where it was easiest to write: inside the resolver for the seat's fare block, during per-subscriber execution. So a corporate seat hold produced 3,187 executions, of which 3,125 completed with the fare block nulled out and were then dropped as empty — or worse, delivered as an empty payload, which quietly told every watcher that *something* had changed on a seat they could not see. The authorization rule was right. Its **placement** was wrong. The cost of the mistake is exactly the fan-out multiplier, which is why this is a scaling question and not a security one. ## Three placements, in increasing cost **1. At the event source.** The node subscribes to a narrow slice — per flight, per cabin, per inventory class — so events nobody on that node is watching never cross the wire. This is the cheapest filter because the work avoided includes the delivery, the deserialization and every execution. Its limit is granularity: the slicing has to be something the event producer can key on before it knows who is listening. **2. Between the event and execution.** For each candidate subscription the server evaluates a predicate over the raw event and that subscription's stored variables and context, and only executes for those that pass. This is where a subscription's own arguments belong. `cabin: ECONOMY` should not be enforced by executing and then discovering the seat was in business class; it should be a comparison against a field on the event. **3. Inside execution.** The predicate runs as part of resolving fields. It is the most expressive placement — it can reach anything a resolver can reach — and the most expensive, because the decision arrives after the work. Reserve it for rules that genuinely cannot be decided any earlier. ## What early filtering demands of the event The predicate can only move earlier if everything it needs is available earlier. That means designing the source event to be *fat enough*: a seat-state event that carries only `{seatId, status}` forces a lookup to discover the cabin, the fare class and the owning booking, which drags you straight back into execution. Adding `cabin`, `fareBucket` and `restrictedTo` to the event costs a few bytes on the backbone and removes thousands of executions. The same applies to the subscriber side: the entitlement check has to be answerable from the stored context. Evaluate the accreditation once at subscribe time, keep the resulting capability in the record, and compare it to `restrictedTo` on the event. Now the decision is a comparison of two values already in memory. ## Keeping it correct when the check moves Moving a check earlier is where correctness bugs get introduced, so state the invariants: - **The rule must not weaken.** An early predicate is an optimisation over the authoritative rule, and the authoritative rule should still hold if it runs. Where a leak would be catastrophic, keep the in-execution check too and treat the early filter purely as a cost reduction. - **A snapshotted entitlement goes stale.** If accreditation is evaluated at subscribe time, a revocation has to reach the stream. Consume the revocation as an event, re-evaluate, and terminate or downgrade the subscription. A stream that outlives its authorization is a real defect, and it is the price of hoisting the check. - **Suppression must be total.** If a subscriber is not entitled to know that a seat changed, sending an empty payload is itself a disclosure. Filtering earlier fixes this for free, which is a correctness argument for the change and not only a cost one. - **The early predicate must not need resolvers.** The moment it calls out to another service per event per subscriber, you have rebuilt the cost you were removing in a worse place. ## Proving it worked Instrument the ratio, not the totals. For each event, record candidate subscriptions, executions actually run, and non-empty deliveries. Before the change the flight's numbers were 3,187 / 3,187 / 62; the useful metric is **executions per non-empty delivery**, which was 51:1. After moving the cabin and entitlement predicates ahead of execution it should sit near 1:1, and any drift back up is a new field or a new rule that has crept into the resolvers. A 4-person platform team that owns this tier can watch one ratio and one hot-entity gauge rather than trying to reason about every product team's subscription. ## Specified versus conventional Nothing here is in the GraphQL specification. The specification describes one operation's per-event execution and stops. Where a filter sits, what an event carries, how entitlement is refreshed and whether identical subscribers are grouped are all implementation decisions — which is precisely why they are interview material: they are judgement, not recall.

  • Is it ever right to leave the authorization check inside execution?
    Yes, as the authoritative copy. An early predicate is an optimisation that can drift from the rule it approximates, so for high-blast-radius data many teams keep the in-execution check and treat the early filter as the thing that stops most executions from happening. The early filter must never be the only place a rule is enforced if a bug there would leak data.
  • What breaks if you evaluate entitlement only when the client subscribes?
    Revocation stops working. The subscription keeps streaming under the capabilities captured at subscribe time, potentially for hours. If you hoist the check, you take on the job of reacting to change: consume a revocation event, re-evaluate the stored capability, and terminate or downgrade the stream. Without that, the optimisation has traded cost for a real authorization hole.
  • Why is dropping the payload after execution not good enough even when the cost is acceptable?
    Because delivering an empty or nulled result still signals that an event occurred on something the subscriber cannot see. Timing alone leaks: a watcher who sees a message every time a hidden corporate seat moves learns the hold pattern. Filtering before execution removes both the cost and the signal; suppressing after execution removes only the signal, and only if you remember to suppress.
  • How would you decide which fields to add to the source event?
    Take the predicates you want to hoist — subscription arguments and coarse visibility keys — and add exactly the fields they compare against, nothing more. The event is not a place to denormalise the whole entity: it grows the backbone payload for every consumer and drifts out of date. Add what removes executions, and leave the rest to be resolved for the subscribers that survive the filter.

saying these in an interview costs you the question

  • Enforces subscription arguments only inside resolvers
  • Treats an emptied payload as a safe non-delivery
  • Hoists an authorization check with no revocation path
  • Adds a per-subscriber service call to the early filter
  • Measures event rate but never executions per delivery
  • Assumes the specification says where filtering belongs

context