Facebook Pixel

2676. Throttle 🔒

Problem Description

This problem asks you to implement a throttle function that controls how often another function can be executed.

The throttle function takes two parameters:

  • fn: a function to be throttled
  • t: a time interval in milliseconds

The throttled function should behave as follows:

  1. First call executes immediately: When the throttled function is called for the first time, it executes fn right away without any delay.

  2. Creates a blocking period: After executing, it creates a "blocking period" of t milliseconds where the function cannot execute again.

  3. Stores the latest arguments during blocking: If the throttled function is called during the blocking period, it doesn't execute immediately but saves the most recent arguments. Multiple calls during the same blocking period will overwrite previous saved arguments with the latest ones.

  4. Executes with latest arguments after delay: Once the blocking period ends, if there were any calls during that period, the function automatically executes with the most recently saved arguments. This execution then starts a new blocking period.

Example walkthrough with t = 50ms:

  • At 30ms: Function called → executes immediately → blocking until 80ms (30 + 50)
  • At 40ms: Function called → can't execute (still blocking) → saves arguments
  • At 60ms: Function called → can't execute (still blocking) → overwrites saved arguments from 40ms call
  • At 80ms: Blocking period ends → executes with arguments from 60ms call → new blocking period until 130ms (80 + 50)

The implementation needs to return a new function that wraps the original function with this throttling behavior, ensuring that the function executes at most once per time interval t, while preserving the most recent call attempt.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To build a throttle mechanism, we need to track whether we're currently in a "blocking period" where execution is not allowed. This suggests using a boolean flag pending that indicates if we're waiting for the time interval to pass.

The key insight is that during the blocking period, we don't want to queue up multiple function calls - we only care about the most recent arguments. This is different from debouncing or queuing patterns. So we need a variable nextArgs to store only the latest arguments that came in during the blocking period.

The flow becomes clear when we think about the two states:

  1. Not blocking (pending = false): Execute the function immediately, set up a timer for t milliseconds, and mark that we're now blocking.

  2. Currently blocking (pending = true): Don't execute, just save the arguments to nextArgs, overwriting any previously saved arguments.

The clever part is what happens when the timer expires. We need to:

  • Clear the blocking flag (pending = false)
  • Check if any calls came in during the blocking period (by checking if nextArgs exists)
  • If there were calls, recursively call the wrapper function with those saved arguments

This recursive call is crucial - it ensures that if arguments were saved during the blocking period, they get executed right when the period ends, and this execution will set up a new blocking period automatically.

By using setTimeout with the wrapper function calling itself when needed, we create a self-managing cycle that handles continuous throttling. Each execution schedules the potential next execution, creating a chain of throttled calls that respects the time interval t while always using the most recent arguments from each blocking period.

Solution Approach

The implementation uses a closure pattern to maintain state between function calls. Here's how each component works:

State Variables:

  • pending: A boolean flag tracking whether we're in a blocking period
  • nextArgs: Stores the most recent arguments received during the blocking period

The Wrapper Function: The returned wrapper function handles all incoming calls:

const wrapper = (...args) => {
    nextArgs = args;  // Always save the latest arguments
  
    if (!pending) {
        // Not in blocking period - execute immediately
        fn(...args);
        pending = true;
        nextArgs = undefined;  // Clear saved args since we just executed
      
        setTimeout(() => {
            pending = false;  // End the blocking period
            if (nextArgs) wrapper(...nextArgs);  // Execute saved call if exists
        }, t);
    }
    // If pending is true, we just save args and do nothing else
};

Implementation Flow:

  1. Every call updates nextArgs: This ensures we always have the latest arguments available.

  2. First execution path (when !pending):

    • Execute fn with current arguments immediately
    • Set pending = true to start blocking period
    • Clear nextArgs since we just executed with current args
    • Set up a timer for t milliseconds
  3. During blocking period (when pending = true):

    • The function only saves arguments to nextArgs
    • Previous saved arguments get overwritten
    • No execution happens, no new timer is set
  4. Timer expiration logic:

    • Reset pending = false to allow new executions
    • Check if nextArgs exists (were there calls during blocking?)
    • If yes, recursively call wrapper(...nextArgs)
    • This recursive call will execute immediately (since pending is now false) and set up a new blocking period

Why this approach works:

  • The closure preserves pending and nextArgs across all calls
  • The self-referential wrapper function enables the recursive behavior
  • Setting nextArgs = undefined after immediate execution prevents duplicate execution when the timer expires
  • The pattern naturally chains executions: each execution that has pending calls will trigger the next one after t milliseconds

This creates a perfect throttling behavior where functions execute at most once per t milliseconds, always using the most recent arguments from each blocking window.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through a concrete example with fn = (x) => console.log(x) and t = 100ms:

Initial State:

  • pending = false (not blocking)
  • nextArgs = undefined (no saved arguments)

Time 0ms: throttled(1) is called

  • nextArgs = [1] (save arguments)
  • pending is false, so enter the if block:
    • Execute fn(1) → outputs 1
    • Set pending = true (start blocking)
    • Clear nextArgs = undefined (we just used these args)
    • Start 100ms timer

Time 30ms: throttled(2) is called (during blocking period)

  • nextArgs = [2] (save arguments)
  • pending is true, so skip the if block
  • Nothing executes, just saved the arguments

Time 60ms: throttled(3) is called (still blocking)

  • nextArgs = [3] (overwrites previous value of [2])
  • pending is true, so skip the if block
  • Nothing executes, arguments from 30ms call are lost

Time 100ms: Timer expires

  • Timer callback executes:
    • Set pending = false (end blocking period)
    • Check nextArgs: it exists with value [3]
    • Call wrapper(3) recursively

Time 100ms: Recursive wrapper(3) execution

  • nextArgs = [3] (save arguments)
  • pending is false, so enter the if block:
    • Execute fn(3) → outputs 3
    • Set pending = true (new blocking period)
    • Clear nextArgs = undefined
    • Start new 100ms timer (expires at 200ms)

Result: The function executed twice - immediately at 0ms with argument 1, and at 100ms with argument 3. The call with argument 2 was discarded because it was overwritten by the more recent call with argument 3 during the same blocking period.

This demonstrates the key throttling behavior: maximum one execution per time interval, with only the most recent arguments from each blocking window being preserved.

Solution Implementation

1import threading
2from typing import Callable, Any
3
4def throttle(fn: Callable[..., Any], t: float) -> Callable[..., Any]:
5    """
6    Creates a throttled version of a function that limits how often it can be called.
7    The throttled function will execute immediately on the first call, then wait for the specified delay.
8    If called again during the delay, it will store the latest arguments and execute once after the delay.
9  
10    Args:
11        fn: The function to throttle
12        t: The throttle delay in milliseconds
13      
14    Returns:
15        A throttled version of the input function
16    """
17    # Flag to track if we're currently in a throttling period
18    is_pending = False
19  
20    # Storage for the most recent arguments received during throttling
21    next_args = None
22  
23    # Lock for thread safety when accessing shared state
24    lock = threading.Lock()
25  
26    def wrapper(*args, **kwargs):
27        """The wrapped function that implements throttling logic"""
28        nonlocal is_pending, next_args
29      
30        with lock:
31            # Always store the latest arguments
32            next_args = (args, kwargs)
33          
34            # If not currently throttling, execute immediately
35            if not is_pending:
36                # Execute the function with current arguments
37                fn(*args, **kwargs)
38              
39                # Enter throttling period
40                is_pending = True
41              
42                # Clear stored arguments since we just executed
43                next_args = None
44              
45                # Set up timer to end throttling period
46                def delayed_execution():
47                    nonlocal is_pending, next_args
48                  
49                    with lock:
50                        # Exit throttling period
51                        is_pending = False
52                      
53                        # If new arguments were received during throttling, execute with them
54                        if next_args is not None:
55                            stored_args, stored_kwargs = next_args
56                            wrapper(*stored_args, **stored_kwargs)
57              
58                # Create timer to execute after delay (convert milliseconds to seconds)
59                timer = threading.Timer(t / 1000.0, delayed_execution)
60                timer.start()
61  
62    return wrapper
63
1import java.util.concurrent.Executors;
2import java.util.concurrent.ScheduledExecutorService;
3import java.util.concurrent.TimeUnit;
4import java.util.function.Consumer;
5
6/**
7 * Functional interface for a function that accepts variable arguments and returns void.
8 * Similar to TypeScript's (...args: any[]) => void
9 */
10@FunctionalInterface
11interface F {
12    void apply(Object... args);
13}
14
15/**
16 * Utility class for creating throttled functions.
17 */
18class ThrottleUtil {
19    // Scheduler for delayed execution
20    private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
21  
22    /**
23     * Creates a throttled version of a function that limits how often it can be called.
24     * The throttled function will execute immediately on the first call, then wait for the specified delay.
25     * If called again during the delay, it will store the latest arguments and execute once after the delay.
26     * 
27     * @param fn - The function to throttle
28     * @param t - The throttle delay in milliseconds
29     * @return A throttled version of the input function
30     */
31    public static F throttle(F fn, int t) {
32        // Create a wrapper class to hold the throttling state
33        class ThrottleWrapper {
34            // Flag to track if we're currently in a throttling period
35            private boolean isPending = false;
36          
37            // Storage for the most recent arguments received during throttling
38            private Object[] nextArgs = null;
39          
40            // The wrapped function that implements throttling logic
41            public void execute(Object... args) {
42                // Always store the latest arguments
43                nextArgs = args;
44              
45                // If not currently throttling, execute immediately
46                if (!isPending) {
47                    // Execute the function with current arguments
48                    fn.apply(args);
49                  
50                    // Enter throttling period
51                    isPending = true;
52                  
53                    // Clear stored arguments since we just executed
54                    nextArgs = null;
55                  
56                    // Set up timer to end throttling period
57                    scheduler.schedule(() -> {
58                        // Exit throttling period
59                        isPending = false;
60                      
61                        // If new arguments were received during throttling, execute with them
62                        if (nextArgs != null) {
63                            execute(nextArgs);
64                        }
65                    }, t, TimeUnit.MILLISECONDS);
66                }
67            }
68        }
69      
70        // Create an instance of the wrapper
71        ThrottleWrapper wrapper = new ThrottleWrapper();
72      
73        // Return a lambda that delegates to the wrapper's execute method
74        return args -> wrapper.execute(args);
75    }
76}
77
1#include <functional>
2#include <chrono>
3#include <thread>
4#include <mutex>
5#include <memory>
6#include <vector>
7#include <any>
8
9// Type definition for a function that accepts any arguments and returns void
10using F = std::function<void(const std::vector<std::any>&)>;
11
12/**
13 * Creates a throttled version of a function that limits how often it can be called.
14 * The throttled function will execute immediately on the first call, then wait for the specified delay.
15 * If called again during the delay, it will store the latest arguments and execute once after the delay.
16 * 
17 * @param fn - The function to throttle
18 * @param t - The throttle delay in milliseconds
19 * @returns A throttled version of the input function
20 */
21F throttle(F fn, int t) {
22    // Shared state between all invocations of the returned function
23    struct ThrottleState {
24        // Flag to track if we're currently in a throttling period
25        bool is_pending = false;
26      
27        // Storage for the most recent arguments received during throttling
28        std::unique_ptr<std::vector<std::any>> next_args = nullptr;
29      
30        // Mutex for thread safety
31        std::mutex mtx;
32    };
33  
34    // Create shared state that persists across invocations
35    auto state = std::make_shared<ThrottleState>();
36  
37    // The wrapped function that implements throttling logic
38    return [fn, t, state](const std::vector<std::any>& args) -> void {
39        std::unique_lock<std::mutex> lock(state->mtx);
40      
41        // Always store the latest arguments
42        state->next_args = std::make_unique<std::vector<std::any>>(args);
43      
44        // If not currently throttling, execute immediately
45        if (!state->is_pending) {
46            // Execute the function with current arguments
47            fn(args);
48          
49            // Enter throttling period
50            state->is_pending = true;
51          
52            // Clear stored arguments since we just executed
53            state->next_args = nullptr;
54          
55            // Set up timer to end throttling period
56            std::thread([fn, t, state]() {
57                // Wait for the throttle delay
58                std::this_thread::sleep_for(std::chrono::milliseconds(t));
59              
60                std::unique_lock<std::mutex> lock(state->mtx);
61              
62                // Exit throttling period
63                state->is_pending = false;
64              
65                // If new arguments were received during throttling, execute with them
66                if (state->next_args != nullptr) {
67                    // Copy the arguments before unlocking to avoid race conditions
68                    auto args_to_execute = *state->next_args;
69                    lock.unlock();
70                  
71                    // Recursively call the throttled function
72                    // This will trigger another throttle cycle if needed
73                    fn(args_to_execute);
74                }
75            }).detach();
76        }
77    };
78}
79
1// Type definition for a function that accepts any arguments and returns void
2type F = (...args: any[]) => void;
3
4/**
5 * Creates a throttled version of a function that limits how often it can be called.
6 * The throttled function will execute immediately on the first call, then wait for the specified delay.
7 * If called again during the delay, it will store the latest arguments and execute once after the delay.
8 * 
9 * @param fn - The function to throttle
10 * @param t - The throttle delay in milliseconds
11 * @returns A throttled version of the input function
12 */
13const throttle = (fn: F, t: number): F => {
14    // Flag to track if we're currently in a throttling period
15    let isPending: boolean = false;
16  
17    // Storage for the most recent arguments received during throttling
18    let nextArgs: any[] | undefined;
19  
20    // The wrapped function that implements throttling logic
21    const wrapper: F = (...args: any[]): void => {
22        // Always store the latest arguments
23        nextArgs = args;
24      
25        // If not currently throttling, execute immediately
26        if (!isPending) {
27            // Execute the function with current arguments
28            fn(...args);
29          
30            // Enter throttling period
31            isPending = true;
32          
33            // Clear stored arguments since we just executed
34            nextArgs = undefined;
35          
36            // Set up timer to end throttling period
37            setTimeout(() => {
38                // Exit throttling period
39                isPending = false;
40              
41                // If new arguments were received during throttling, execute with them
42                if (nextArgs !== undefined) {
43                    wrapper(...nextArgs);
44                }
45            }, t);
46        }
47    };
48  
49    return wrapper;
50};
51

Time and Space Complexity

Time Complexity: O(1) for each throttled function call. The throttle wrapper function performs a constant amount of work per invocation - it updates variables (nextArgs, pending), potentially calls the original function fn, and sets up a setTimeout. The setTimeout callback also performs O(1) operations. While the original function fn may have its own time complexity, the throttling mechanism itself adds only constant overhead.

Space Complexity: O(1) for the throttle mechanism itself. The implementation maintains a fixed amount of state regardless of how many times the throttled function is called:

  • One boolean flag pending
  • One variable nextArgs that stores the most recent arguments
  • One closure scope containing the wrapper function
  • One setTimeout callback in the event queue at most

The space used doesn't grow with the number of calls. Even if the throttled function is called many times while pending is true, only the most recent arguments are stored in nextArgs, replacing any previous values. Therefore, the space complexity remains constant.

Common Pitfalls

1. Race Condition with Shared State

The most critical pitfall in throttle implementations is improper handling of concurrent access to shared state variables (is_pending and next_args). Without proper synchronization, multiple threads calling the throttled function simultaneously can cause:

  • Lost updates to next_args
  • Incorrect is_pending state leading to multiple executions within the throttling window
  • Missed executions of saved arguments

Solution: Use thread-safe mechanisms like locks (as shown in the Python implementation) or atomic operations to protect critical sections where shared state is read and modified.

2. Memory Leak from Timer References

Each timer created holds a reference to the wrapper function and its closure. If the throttled function is called frequently but the timers aren't properly cleaned up (especially in long-running applications), this can lead to:

  • Accumulation of timer objects in memory
  • Potential execution of outdated callbacks if timers aren't cancelled

Solution: Keep a reference to the active timer and cancel it when necessary:

def throttle(fn, t):
    is_pending = False
    next_args = None
    active_timer = None
    lock = threading.Lock()
  
    def wrapper(*args, **kwargs):
        nonlocal is_pending, next_args, active_timer
      
        with lock:
            next_args = (args, kwargs)
          
            if not is_pending:
                fn(*args, **kwargs)
                is_pending = True
                next_args = None
              
                def delayed_execution():
                    nonlocal is_pending, next_args, active_timer
                    with lock:
                        is_pending = False
                        active_timer = None
                        if next_args is not None:
                            stored_args, stored_kwargs = next_args
                            wrapper(*stored_args, **stored_kwargs)
              
                active_timer = threading.Timer(t / 1000.0, delayed_execution)
                active_timer.start()
  
    # Add cleanup method
    def cleanup():
        nonlocal active_timer
        if active_timer:
            active_timer.cancel()
  
    wrapper.cleanup = cleanup
    return wrapper

3. Incorrect Argument Preservation

A common mistake is not properly clearing next_args after immediate execution. If you forget to set next_args = None after the immediate execution, the same arguments will be executed again when the timer expires, causing duplicate function calls.

Solution: Always clear next_args immediately after executing the function in the non-pending branch to prevent duplicate execution.

4. Lost Context in Callbacks

In object-oriented contexts, the throttled function might lose its this context (in JavaScript) or self reference (in Python methods), leading to errors when accessing instance attributes.

Solution: Preserve the context properly:

# For instance methods
def throttle_method(fn, t):
    def decorator(self, *args, **kwargs):
        # Bind fn to self to preserve context
        bound_fn = lambda *a, **k: fn(self, *a, **k)
        return throttle(bound_fn, t)(*args, **kwargs)
    return decorator

5. Timing Precision Issues

Converting milliseconds to seconds (dividing by 1000) can introduce floating-point precision errors, especially with very small or very large time intervals.

Solution: Use high-precision timing libraries or validate input ranges:

import time

def precise_throttle(fn, t_ms):
    last_execution_time = 0
  
    def wrapper(*args, **kwargs):
        nonlocal last_execution_time
        current_time = time.perf_counter() * 1000  # Convert to milliseconds
      
        if current_time - last_execution_time >= t_ms:
            fn(*args, **kwargs)
            last_execution_time = current_time
  
    return wrapper
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many times is a tree node visited in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More