What is demand and backpressure in Combine, and how does a custom subscriber control it?

11.1.2 · L3 · Interview

Predict first

A custom subscriber requests two values and returns no extra demand. What does it receive from a five-value publisher?

import Combine

final class Sub: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    var got: [Int] = []
    func receive(subscription: Subscription) { subscription.request(.max(2)) }
    func receive(_ i: Int) -> Subscribers.Demand { got.append(i); return .none }
    func receive(completion: Subscribers.Completion<Never>) { got.append(-1) }
}
let s = Sub()
(1...5).publisher.subscribe(s)
print(s.got)
  • [1, 2]
  • [1, 2, 3, 4, 5, -1]
  • [1, 2, -1]

Predict first

The same subscriber returns .max(1) after each value, starting from one request:

import Combine

final class Sub: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    var got: [Int] = []
    func receive(subscription: Subscription) { subscription.request(.max(1)) }
    func receive(_ i: Int) -> Subscribers.Demand { got.append(i); return .max(1) }
    func receive(completion: Subscribers.Completion<Never>) { got.append(-1) }
}
let s = Sub()
(1...5).publisher.subscribe(s)
print(s.got)
  • [1]
  • [1, 2, 3, 4, 5, -1]
  • [1, 2]

Mental model. Demand is the consumer holding up fingers: “send me two”. A publisher that has sent two stops and waits, however much it has waiting, until the consumer holds up more.

Backpressure is the mechanism by which a slow consumer limits a fast producer. In Combine it is demand: a Subscribers.Demand is a count (.max(n)), .unlimited or .none, and a publisher may never send more values than the total demand requested so far. The subscriber requests demand twice: once up front (subscription.request(...) in receive(subscription:)) and again by returning a Demand from each receive(_ input:) call, which is added to what is outstanding. So a subscriber that requests .max(1) and returns .max(1) each time receives values one by one at its own speed, while one that requests .max(2) and returns .none gets exactly two values and then nothing more (and never a completion, because the publisher is waiting for more demand).

In practice sink and assign request .unlimited, so most pipelines have no backpressure and a fast producer simply pushes everything. That is fine for UI events but a problem for large or fast sources (reading a big file, a high-rate sensor), where you need a custom subscriber, a flatMap(maxPublishers:) to limit inner concurrency, or operators that change the pace: buffer(size:prefetch:whenFull:) to absorb bursts (with a policy for overflow: drop or fail), throttle and debounce to reduce rate, and collect to batch. Remember that most system publishers (NotificationCenter, Timer) do not truly honour demand: they drop values when demand is zero. Compared with AsyncSequence, whose consumer pulls one element at a time by calling next(), backpressure there is automatic, which is one reason the async version is simpler for sequential consumption.

Only two values were requested, so the publisher sends two and then waits. No completion arrives because demand never came back.

Each returned demand asks for one more, so the values flow one at a time until the publisher completes.

Common traps

  • Assuming sink applies backpressure. It requests unlimited demand, so a fast producer floods the consumer.
  • Returning .none and expecting more values. No new demand means the publisher waits forever and never completes.

They might follow up with

  • How do you limit how many inner requests a flatMap runs? flatMap(maxPublishers: .max(n)).
  • How does the async version differ? The consumer pulls each element with next(), so pace is built in.

Related questions

  • 11.1.1 What are Publisher, Subscriber, Subscription and Subscriber.Input/Failure, and when does a pipeline actually…
  • 11.2.2 How do flatMap and switchToLatest differ, and when do you use each?
  • 11.6.1 When do you use Combine, AsyncSequence or the Observation framework, and when should you not use Combine?

← What are Publisher, Subscriber, Subscription and…