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?
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
throttlea 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… →