RxJS: what debounce and throttle really do

Eugene Ghanizadeh
CONNECT platform
Published in
3 min readJun 26, 2019

P.S. ReactiveExtensions* please document your stuff properly

Recently I was reading up on RxJS’s debounce() and throttle() operators, trying to figure out how they exactly work. The typical place to check this information is this nice website called learnrxjs.io, which in this case gives this definition for debounce():

Discard emitted values that take less than the specified time, based on selector function, between output.

and the following for throttle() :

Emit value on the leading edge of an interval, but suppress new values until durationSelector has completed.

Both description seem to indicate that you somehow need to specify some sort of time-span or interval for these operators, however, a look at the examples provided in the same site shows that this is indeed wrong:

const debouncedExample = example.pipe(debounce(() => timer(1000)));const throttleExample = source.pipe(throttle(val => interval(2000)));

As you can see in these examples, it is the internal Observables that care about “time” and “interval”s, not debounce() and throttle() themselves. This made me curious to see what exactly would happen if I passed some other non-timer-related Observables to these operators. As it turned out, neither debounce() nor throttle() have anything to do with timers and intervals:

As it turns out, debounce() actually creates a control observable for every value that the source emits, using the factory function you provide it with. If the source emits another value before this control observable completes, it will simply throw out the control observable, alongside its corresponding value, and create a new control observable for the newly emitted value. However if the control observable completes before the source emits again, then that value will be emitted.

In other words, debounce() will create one Observable per value emitted by the source, and will only emit that value if that Observable completes earlier than the next emission from the source. The only exception to this rule is when the source itself completes, in which case its last emitted value will also be emitted by debounce() .

Similarly, throttle() creates a control observable for a value emitted by the source. After that, it will ignore all other emissions by the source until that control observable completes, after which it will emit the original value that the control observable was created for. Note that this also means that no control observables are created for the ignored values. Lastly, similar to debounce() , it will emit the last value of the source when the source completes.

Fun Fact: None of these snippets will work if you remove the setImmediate()s, or if you put all the emissions in the same function body. I suspect that’s due to some sort of race condition which will cause the values to be ignored, but either way it simply means that if you try to cook up some synchronous process using debounce() or throttle(), you will most probably fail as you will find them not behaving as you would expect.

--

--

Eugene Ghanizadeh
CONNECT platform

Programmer, Designer, Product Manager, even at some point an HR-ish guy. Also used to be a Teacher. https://github.com/loreanvictor