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 throttledt
: a time interval in milliseconds
The throttled function should behave as follows:
-
First call executes immediately: When the throttled function is called for the first time, it executes
fn
right away without any delay. -
Creates a blocking period: After executing, it creates a "blocking period" of
t
milliseconds where the function cannot execute again. -
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.
-
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 until80ms
(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 until130ms
(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.
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:
-
Not blocking (
pending = false
): Execute the function immediately, set up a timer fort
milliseconds, and mark that we're now blocking. -
Currently blocking (
pending = true
): Don't execute, just save the arguments tonextArgs
, 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 periodnextArgs
: 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:
-
Every call updates
nextArgs
: This ensures we always have the latest arguments available. -
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
- Execute
-
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
- The function only saves arguments to
-
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
- Reset
Why this approach works:
- The closure preserves
pending
andnextArgs
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 EvaluatorExample 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
isfalse
, so enter the if block:- Execute
fn(1)
→ outputs1
- Set
pending = true
(start blocking) - Clear
nextArgs = undefined
(we just used these args) - Start 100ms timer
- Execute
Time 30ms: throttled(2)
is called (during blocking period)
nextArgs = [2]
(save arguments)pending
istrue
, 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
istrue
, 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
- Set
Time 100ms: Recursive wrapper(3)
execution
nextArgs = [3]
(save arguments)pending
isfalse
, so enter the if block:- Execute
fn(3)
→ outputs3
- Set
pending = true
(new blocking period) - Clear
nextArgs = undefined
- Start new 100ms timer (expires at 200ms)
- Execute
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
How many times is a tree node visited in a depth first search?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!