Lodash Throttle and System Clock Changes
This article explains how the Lodash JavaScript library executes
_.throttle when the operating system clock is adjusted
during runtime. It covers the internal mechanics of Lodash's timing
checks, how its implementation relies on the wall-clock time via
Date.now(), and how the codebase explicitly detects and
mitigates both forward and backward system clock shifts to avoid
infinite execution delays.
The Foundation: Throttle via Debounce
In Lodash, _.throttle is not a distinct implementation;
it is a wrapper around _.debounce. Calling
_.throttle(func, wait, options) invokes
_.debounce with specific configuration defaults:
_.throttle = function(func, wait, options) {
var leading = true,
trailing = true;
if (typeof options === 'object') {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
};Lodash tracks timestamps using Date.now() rather than a
monotonic clock such as performance.now(). Because
Date.now() reflects the system's wall-clock time, any
manual adjustment or Network Time Protocol (NTP) correction to the
operating system clock directly affects the calculations Lodash performs
to determine whether an invocation is due.
How Lodash
Evaluates Execution: shouldInvoke
Whenever the throttled wrapper is invoked, or when an internal
setTimeout fires, Lodash calls an internal helper named
shouldInvoke(time).
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
return (lastCallTime === undefined ||
(timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) ||
(maxing && timeSinceLastInvoke >= maxWait));
}This logic dictates the exact behavior when the system clock shifts.
Scenario 1: The System Clock Moves Forward
If the system clock is manually adjusted forward (for example, jumped ahead by 10 minutes):
- Calculated Elapsed Time Increases: The variable
time(derived fromDate.now()) will be substantially larger thanlastCallTimeorlastInvokeTime. - Immediate Execution Condition: The check
timeSinceLastCall >= waitortimeSinceLastInvoke >= maxWaitinstantly evaluates totrue. - Behavior: The throttled function triggers
immediately on the next invocation. If a trailing edge was pending via
an active
setTimeout, the callback checksshouldInvoke(time), sees that the duration has elapsed, and executes the target function without further delay.
Scenario 2: The System Clock Moves Backward
If the system clock is manually turned backward (for example, turned back by one hour), a naive timing library would stall, waiting for the system clock to catch back up to the stored timestamp. Lodash prevents this stall with a dedicated condition:
(timeSinceLastCall < 0)- Negative Delta Detection: When the clock is set
backward,
timeis less thanlastCallTime. As a result,time - lastCallTimeresults in a negative number. - Forced Invocation: Because
timeSinceLastCall < 0evaluates totrue,shouldInvokeimmediately returnstrue. - Timestamp Reset: When
shouldInvoketriggers execution,lastInvokeTimeandlastCallTimeare updated to the new, backward-shiftedDate.now(). - Behavior: Instead of hanging indefinitely until the real-world time reaches the previously recorded timestamp, Lodash treats the backward clock change as a trigger to execute the function and reset its internal state to the new system time.
Interaction with Native
setTimeout
While Lodash calculates time differences using
Date.now(), it schedules future checks using native
setTimeout. In modern runtimes (V8 in Node.js and Chrome,
SpiderMonkey, JavaScriptCore), setTimeout generally
operates on an internal monotonic clock.
If a timer was already scheduled before the clock shifted:
- The timer still fires relative to actual elapsed physical time.
- When the timer callback executes, it recalculates the remaining
delay using
Date.now(). - If it detects that the clock changed backward
(
timeSinceLastCall < 0), it bypasses further delay and invokes the function immediately. - If it detects that the clock changed forward,
remainingWaitevaluates to0or less, also triggering immediate execution.