What do debounce, throttle(latest: true) and throttle(latest: false) each emit for a burst of events, and which do you use for search, scrolling and taps?

11.9.3 · L3 · Interview

Predict first

Five values at 40 ms intervals go through debounce(150 ms) and two throttles (200 ms). What does each emit?

import Combine
import Foundation

var bag = Set<AnyCancellable>()
let q = DispatchQueue.main
let s = PassthroughSubject<Int, Never>()
var debounced: [Int] = [], latest: [Int] = [], first: [Int] = []
s.debounce(for: .milliseconds(150), scheduler: q).sink { debounced.append($0) }.store(in: &bag)
s.throttle(for: .milliseconds(200), scheduler: q, latest: true).sink { latest.append($0) }.store(in: &bag)
s.throttle(for: .milliseconds(200), scheduler: q, latest: false).sink { first.append($0) }.store(in: &bag)
for i in 1...5 { q.asyncAfter(deadline: .now() + .milliseconds(20 + (i - 1) * 40)) { s.send(i) } }
q.asyncAfter(deadline: .now() + 0.8) {
    print("debounce:", debounced)
    print("throttle latest:", latest)
    print("throttle first:", first)
    exit(0)
}
RunLoop.main.run()
  • debounce: [5] / throttle latest: [1, 5] / throttle first: [1, 2]
  • debounce: [1, 2, 3, 4, 5] / throttle latest: [1, 5] / throttle first: [1, 5]
  • debounce: [1] / throttle latest: [1] / throttle first: [1]

Mental model. Debounce is a lift that waits until nobody has pressed the button for a moment before it leaves. Throttle is a turnstile that lets one person through per interval, whichever one it chooses.

Take five values sent 40 ms apart. debounce(for: 150 ms) waits for a quiet gap and emits only the last value of the burst, after the gap: [5]. It never emits while events keep arriving faster than the interval, so it suits “user has stopped typing” (search-as-you-type). throttle(for: 200 ms, latest: true) emits the first value immediately, then at most one value per interval, choosing the latest in each window: [1, 5]. throttle(…, latest: false) also emits at most one per window, but picks the first value of that window: [1, 2].

So: use debounce when you only care about the settled value and want to avoid work during a burst (search, validation, layout-driven requests); use throttle when you want regular updates during a continuous stream (scroll position, a slider driving a preview, location updates) with a bounded rate, and latest: true when the freshest value matters more than the first; use latest: false for “take the first tap and ignore the rest for a while” (anti-double-tap) though a button-disable state is often better. Both need a scheduler that is running (Q313 on testing). Also consider removeDuplicates after debounce so a user who types and deletes the same text does not trigger a duplicate request.

Debounce emits the last value after the quiet gap; throttle emits per window, the latest or the first of that window.

Common traps

  • Using throttle for search-as-you-type. It fires in the middle of typing; debounce waits for the pause.
  • Using debounce on a continuous stream. If events never pause, it never emits.

They might follow up with

  • How would you test the timing? With an injected scheduler and virtual time, not by sleeping (Q313).
  • Is throttle a rate limit for a server? It limits client-side emission only; the server still needs its own limits.

Related questions

  • 11.9.2 What is the difference between receive(on: ImmediateScheduler) and receive(on: DispatchQueue.main) for the…
  • 11.9.4 Why does Timer.publish emit nothing until the run loop runs, and which run-loop mode should it use?
  • 11.2.3 What do debounce, throttle and removeDuplicates do, and what do share and multicast change?
  • 11.6.2 How do you test Combine pipelines, and what is a TestScheduler pattern?

← What is the difference between receive(on: ImmediateScheduler) and…   Why does Timer.publish emit nothing until the run loop runs, and… →