skip to content

An adapter over a push feed accepts demand but emits every value regardless — what does that break?

level: seniorimportance: should knowfreq 44%

answer

  1. accepting demand is a promise
  2. only as honest as its weakest stage
  3. excess lands on the slowest stage
  4. symptom far from the cause
  5. compare emitted against granted

basics

~20 s

A dishonest adapter breaks the guarantee the rest of the pipeline is sized against: downstream stages assume no more than outstanding demand arrives. The excess then lands wherever the pipeline is slowest, so the symptom appears far from its cause.

solid answer

~50 s

Every stage downstream of that adapter was written on the assumption that it receives at most what it asked for — that is what lets it size its own working set and skip an internal queue. An adapter that accepts requests and then emits on the source's schedule has quietly withdrawn that guarantee without telling anyone, so the pipeline is only as protected as its least honest stage. The excess does not vanish: it accumulates at whichever stage is actually slow, which is usually nowhere near the boundary, and the incident is investigated at the wrong component. The fix is not another buffer after the adapter — that just relocates the lie. It is to make the adapter honour demand and apply a declared policy, so the loss is chosen at the boundary and counted there.

code

pseudocode · 14 lines
pseudocode
# dishonest: demand is accepted, then ignored
on request(n):
    outstanding = outstanding + n
on push(value):
    emit(value)                    # fires even when outstanding is 0

# honest: same arrival path, bounded by outstanding demand
on push(value):
    if outstanding > 0:
        emit(value)
        outstanding = outstanding - 1
    else:
        latest = value             # declared policy
        discarded = discarded + 1  # and counted at the boundary

go deeper

for a junior

Understand that requesting a number of values is a promise both sides keep, and that a component which accepts the request and ignores it has broken the pipeline's main guarantee.

for a middle

Explain why downstream stages contain no flow-control logic of their own, and what they must start doing once one upstream component over-emits.

for a senior

Diagnose from the signature: growth that follows the source's rate rather than the consumer's latency, and counters at the boundary that show emitted exceeding granted.

for a principal

Decide where this guarantee is enforced as a matter of design — one honest boundary component per external feed beats defensive buffering scattered through every pipeline.

## What the demand contract is actually buying The demand protocol is not primarily about politeness between two components. Its value is that **every stage downstream can be written against a bound**. If a stage requests thirty-two values, it can allocate for thirty-two, process them, and ask again; it needs no internal queue and no policy for what to do when overwhelmed, because being overwhelmed is not in its contract. That is why a demand-driven pipeline can be assembled from small stages that individually contain no flow-control logic at all. An adapter that accepts demand and emits regardless withdraws that guarantee for the whole pipeline, silently. Nothing in the pipeline's definition changes, no type differs, and no stage is notified. What changes is that a property everything relied on is now false. ## Where the excess actually goes The values that were never requested are not absorbed by the contract's absence — they move. - They accumulate at **whichever stage is genuinely slow**, which may be several hops downstream: a stage doing input/output, a stage doing heavy computation, a stage waiting on something external. - That stage grows an internal queue, or blocks, or in a lossy design discards — quietly. - Memory, latency and failure therefore appear at a component that is behaving correctly, while the component that caused it looks healthy by every local metric. This is the diagnostic signature worth remembering: **the growth tracks the source's activity, not the consumer's latency**. If the pressure were an honest slow-consumer problem, the queue would grow when the consumer slows. When an adapter over-emits, the queue grows when the *feed* gets busy, regardless of what the consumer is doing. The two look identical on a memory graph and are told apart by correlating with the source's rate. ## Why the obvious fixes are not fixes 1. **Add a buffer after the adapter.** This relocates the violation by one hop. The buffer either has a bound — in which case the loss decision is now made by a component with no idea what the values mean — or it does not, in which case nothing has changed except where the memory sits. 2. **Block the thread the feed calls you on.** It genuinely applies pressure, but into a component you do not own. The other party's delivery machinery now stalls, and the loss typically reappears in a place with no instrumentation you can read. It is occasionally the right call, and it is never a safe default. 3. **Increase the consumer's request size.** This makes the violation less frequent rather than less real, and it hides the problem exactly until the day the source bursts. The actual fix is boring: the adapter tracks outstanding demand, emits only within it, and applies a policy — discard, keep the latest, a bounded hold, or an aggregate — to anything that arrives outside it. The pipeline's guarantee is restored because the one component that could not honour it honestly declares what it does instead. ## How to catch it The boundary should expose three counters, and their relationship is the test: - **arrivals** — values delivered by the source. - **granted** — total demand received from downstream. - **emitted** — values passed on. If `emitted` ever exceeds `granted`, the adapter is lying, and no further investigation of downstream stages is warranted until that is fixed. If `arrivals` minus `emitted` grows without a corresponding discard or overwrite count, something is holding values you did not decide to hold. ## What honesty does not mean Honouring demand is not the same as losing nothing. An adapter that discards nine values out of ten is perfectly honest if that is its declared policy and the discards are counted; a consumer sampling a continuous quantity may want exactly that. The dishonest adapter is not the lossy one — it is the one whose behaviour cannot be read off its contract. That distinction is what separates a candidate who has run a pipeline like this from one who has only read about backpressure: the first talks about what was decided and measured at the boundary, the second talks about avoiding loss and ends up with an unbounded hold.

  • How would you detect an over-emitting adapter from outside the component?
    Compare three counters at the boundary: arrivals from the source, demand granted by downstream, and values emitted. Emitted exceeding granted proves the adapter lies. From further away, the signature is a stage whose queue depth and memory track the source's rate rather than the consumer's latency — an honest slow-consumer problem grows when the consumer slows, not when the feed gets busy.
  • The feed's own thread calls you. Why not simply block it until demand arrives?
    Because the pause then lands inside a component you do not own. The other party's delivery machinery stalls, and the loss usually reappears in their buffers, where you have no counter and no policy. It is occasionally the right trade when the caller is yours and blocking is cheap, but as a default it converts a decision you could have made explicitly into one someone else makes invisibly.

saying these in an interview costs you the question

  • Says the source ignores demand, so the adapter may too
  • Blames the downstream stage where the memory happened to grow
  • Fixes it by inserting another buffer just after the adapter
  • Thinks over-emitting is harmless as long as nothing crashes
  • Assumes blocking the feed's own callback thread is a safe default