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):

  1. Calculated Elapsed Time Increases: The variable time (derived from Date.now()) will be substantially larger than lastCallTime or lastInvokeTime.
  2. Immediate Execution Condition: The check timeSinceLastCall >= wait or timeSinceLastInvoke >= maxWait instantly evaluates to true.
  3. Behavior: The throttled function triggers immediately on the next invocation. If a trailing edge was pending via an active setTimeout, the callback checks shouldInvoke(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)
  1. Negative Delta Detection: When the clock is set backward, time is less than lastCallTime. As a result, time - lastCallTime results in a negative number.
  2. Forced Invocation: Because timeSinceLastCall < 0 evaluates to true, shouldInvoke immediately returns true.
  3. Timestamp Reset: When shouldInvoke triggers execution, lastInvokeTime and lastCallTime are updated to the new, backward-shifted Date.now().
  4. 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: