Facebook Pixel

359. Logger Rate Limiter 🔒

EasyDesignHash TableData Stream
Leetcode Link

Problem Description

You need to design a logger system that handles incoming messages with timestamps. The key requirement is that each unique message should only be printed at most once every 10 seconds.

Here's how the system works:

  • When a message arrives at timestamp t and gets printed, any identical message that arrives within the next 10 seconds (before timestamp t + 10) should not be printed
  • Messages always arrive in chronological order (timestamps are non-decreasing)
  • Multiple messages can arrive at the same timestamp

You need to implement a Logger class with two methods:

  1. Logger(): Initializes the logger object
  2. shouldPrintMessage(timestamp, message): Takes a timestamp (integer) and a message (string) as input, and returns:
    • true if the message should be printed (either it's the first time seeing this message, or it's been at least 10 seconds since it was last printed)
    • false if the message should not be printed (it was printed less than 10 seconds ago)

For example:

  • If message "foo" is printed at timestamp 1, it cannot be printed again until timestamp 11 or later
  • If the same "foo" arrives at timestamps 2, 5, or 10, the method should return false
  • If "foo" arrives at timestamp 11 or later, it can be printed again (method returns true)

The solution uses a hash map (limiter) to track each message and the earliest timestamp when it can be printed again. When a message should be printed, the map is updated with timestamp + 10 to block printing for the next 10 seconds.

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

Intuition

The core challenge is tracking when each message was last printed to determine if enough time has passed for it to be printed again. Since we need to check if at least 10 seconds have elapsed since the last print, we need to remember timing information for each unique message.

The natural approach is to use a hash map where:

  • The key is the message string
  • The value represents timing information

But what timing information should we store? We have two choices:

  1. Store the timestamp when the message was last printed
  2. Store the timestamp when the message can be printed again

The second option is more elegant because it simplifies our logic. Instead of storing the last print time and calculating last_print_time + 10 > current_timestamp each time, we directly store last_print_time + 10 as the "expiry time" or "next allowed time".

This way, our check becomes straightforward:

  • If the current timestamp is less than the stored value, the message is still "blocked" → return false
  • If the current timestamp is greater than or equal to the stored value (or the message doesn't exist in our map), the message can be printed → return true and update the map with current_timestamp + 10

The beauty of this approach is that we don't need to clean up old entries from the hash map. Since messages arrive in chronological order, old entries naturally become outdated and will always pass the timestamp check when encountered again. The map serves as a simple rate limiter that tracks the "cooling period" for each message.

Solution Approach

The solution uses a hash map-based approach to implement the rate limiting functionality. Let's walk through the implementation step by step:

Data Structure:

  • We use a single hash map called limiter where:
    • Key: message string
    • Value: the earliest timestamp when this message can be printed again

Initialization:

def __init__(self):
    self.limiter = {}

We initialize an empty dictionary to store our message-timestamp mappings.

Core Logic in shouldPrintMessage:

  1. Retrieve the stored timestamp for the message:

    t = self.limiter.get(message, 0)

    We use get(message, 0) which returns:

    • The stored timestamp if the message exists in the map
    • 0 as default if the message has never been seen before
  2. Check if the message should be printed:

    if t > timestamp:
        return False

    If the stored "next allowed time" (t) is greater than the current timestamp, it means we're still within the 10-second blocking period, so we return False.

  3. Update the map and return True:

    self.limiter[message] = timestamp + 10
    return True

    If we reach this point, the message can be printed. We:

    • Update the map with timestamp + 10 to block this message for the next 10 seconds
    • Return True to indicate the message should be printed

Why this works:

  • For a new message: t = 0 is always less than any positive timestamp, so it passes the check
  • For a repeated message: The stored value t represents when it can next be printed. If the current timestamp hasn't reached that point yet (timestamp < t), we block it
  • The chronological order guarantee means we never have to handle cases where timestamps go backwards

Time and Space Complexity:

  • Time: O(1) for each shouldPrintMessage call (hash map lookup and update)
  • Space: O(M) where M is the number of unique messages seen so far

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 sequence of messages to see how the solution works:

Initial state: limiter = {} (empty hash map)

Message 1: shouldPrintMessage(1, "foo")

  • Check: t = limiter.get("foo", 0) = 0
  • Since 0 <= 1, the message can be printed
  • Update: limiter["foo"] = 1 + 10 = 11
  • Return: true
  • State: limiter = {"foo": 11}

Message 2: shouldPrintMessage(2, "bar")

  • Check: t = limiter.get("bar", 0) = 0
  • Since 0 <= 2, the message can be printed
  • Update: limiter["bar"] = 2 + 10 = 12
  • Return: true
  • State: limiter = {"foo": 11, "bar": 12}

Message 3: shouldPrintMessage(3, "foo")

  • Check: t = limiter.get("foo", 0) = 11
  • Since 11 > 3, the message is still blocked
  • Return: false
  • State: limiter = {"foo": 11, "bar": 12} (unchanged)

Message 4: shouldPrintMessage(8, "bar")

  • Check: t = limiter.get("bar", 0) = 12
  • Since 12 > 8, the message is still blocked
  • Return: false
  • State: limiter = {"foo": 11, "bar": 12} (unchanged)

Message 5: shouldPrintMessage(11, "foo")

  • Check: t = limiter.get("foo", 0) = 11
  • Since 11 <= 11, the message can be printed (exactly at the boundary)
  • Update: limiter["foo"] = 11 + 10 = 21
  • Return: true
  • State: limiter = {"foo": 21, "bar": 12}

Message 6: shouldPrintMessage(12, "bar")

  • Check: t = limiter.get("bar", 0) = 12
  • Since 12 <= 12, the message can be printed
  • Update: limiter["bar"] = 12 + 10 = 22
  • Return: true
  • State: limiter = {"foo": 21, "bar": 22}

The key insight is that we store the next allowed timestamp (current + 10) rather than the last printed timestamp. This makes our check simple: if the current timestamp is less than the stored value, the message is still in its "cooling period" and should be blocked.

Solution Implementation

1class Logger:
2    def __init__(self):
3        """
4        Initialize your data structure here.
5        """
6        # Dictionary to store message -> next_allowed_timestamp mapping
7        self.message_limiter = {}
8
9    def shouldPrintMessage(self, timestamp: int, message: str) -> bool:
10        """
11        Returns true if the message should be printed in the given timestamp, otherwise returns false.
12        If this method returns false, the message will not be printed.
13        The timestamp is in seconds granularity.
14        """
15        # Get the next allowed timestamp for this message (default to 0 if not seen before)
16        next_allowed_time = self.message_limiter.get(message, 0)
17      
18        # Check if current timestamp is before the next allowed time
19        if next_allowed_time > timestamp:
20            # Message is still in cooldown period, don't print
21            return False
22      
23        # Message can be printed, update the next allowed time to current + 10 seconds
24        self.message_limiter[message] = timestamp + 10
25        return True
26
27
28# Your Logger object will be instantiated and called as such:
29# obj = Logger()
30# param_1 = obj.shouldPrintMessage(timestamp, message)
31
1class Logger {
2
3    // HashMap to store message and its next allowed timestamp
4    private Map<String, Integer> messageTimestampMap;
5
6    /**
7     * Initialize your data structure here.
8     */
9    public Logger() {
10        messageTimestampMap = new HashMap<>();
11    }
12
13    /**
14     * Returns true if the message should be printed in the given timestamp, otherwise returns
15     * false. If this method returns false, the message will not be printed. The timestamp is in
16     * seconds granularity.
17     * 
18     * @param timestamp The current timestamp in seconds
19     * @param message The message to be printed
20     * @return true if the message should be printed, false otherwise
21     */
22    public boolean shouldPrintMessage(int timestamp, String message) {
23        // Get the next allowed timestamp for this message (0 if message hasn't been seen before)
24        int nextAllowedTimestamp = messageTimestampMap.getOrDefault(message, 0);
25      
26        // If current timestamp is before the next allowed timestamp, reject the message
27        if (nextAllowedTimestamp > timestamp) {
28            return false;
29        }
30      
31        // Update the next allowed timestamp for this message (current + 10 seconds)
32        messageTimestampMap.put(message, timestamp + 10);
33      
34        // Allow the message to be printed
35        return true;
36    }
37}
38
39/**
40 * Your Logger object will be instantiated and called as such:
41 * Logger obj = new Logger();
42 * boolean param_1 = obj.shouldPrintMessage(timestamp,message);
43 */
44
1#include <unordered_map>
2#include <string>
3
4class Logger {
5private:
6    // Hash map to store message and its next allowed timestamp
7    // Key: message string
8    // Value: timestamp when the message can be printed again
9    std::unordered_map<std::string, int> limiter;
10  
11public:
12    /**
13     * Initialize your data structure here.
14     * Constructor resets the limiter to an empty state
15     */
16    Logger() {
17        limiter.clear();
18    }
19  
20    /**
21     * Returns true if the message should be printed in the given timestamp, otherwise returns false.
22     * If this method returns false, the message will not be printed.
23     * The timestamp is in seconds granularity.
24     * Messages are rate-limited to once every 10 seconds.
25     * 
26     * @param timestamp - The current timestamp in seconds
27     * @param message - The message to be printed
28     * @return true if the message should be printed, false otherwise
29     */
30    bool shouldPrintMessage(int timestamp, std::string message) {
31        // Check if the message exists in the limiter
32        if (limiter.find(message) != limiter.end()) {
33            // Get the next allowed timestamp for this message
34            int nextAllowedTime = limiter[message];
35          
36            // Check if current timestamp is before the next allowed time
37            if (nextAllowedTime > timestamp) {
38                return false;
39            }
40        }
41      
42        // Update the next allowed time for this message (current time + 10 seconds)
43        limiter[message] = timestamp + 10;
44        return true;
45    }
46};
47
1/**
2 * Interface to define the structure of the message limiter
3 * Key: message string
4 * Value: timestamp when the message can be printed again
5 */
6interface MessageLimiter {
7    [message: string]: number;
8}
9
10/**
11 * Global limiter object to track message timestamps
12 */
13let limiter: MessageLimiter = {};
14
15/**
16 * Initialize your data structure here.
17 * Resets the limiter to an empty state
18 */
19const Logger = function(): void {
20    limiter = {};
21};
22
23/**
24 * Returns true if the message should be printed in the given timestamp, otherwise returns false.
25 * If this method returns false, the message will not be printed.
26 * The timestamp is in seconds granularity.
27 * Messages are rate-limited to once every 10 seconds.
28 * 
29 * @param timestamp - The current timestamp in seconds
30 * @param message - The message to be printed
31 * @returns true if the message should be printed, false otherwise
32 */
33const shouldPrintMessage = function(timestamp: number, message: string): boolean {
34    // Get the next allowed timestamp for this message (default to 0 if not exists)
35    const nextAllowedTime: number = limiter[message] || 0;
36  
37    // Check if current timestamp is before the next allowed time
38    if (nextAllowedTime > timestamp) {
39        return false;
40    }
41  
42    // Update the next allowed time for this message (current time + 10 seconds)
43    limiter[message] = timestamp + 10;
44    return true;
45};
46

Time and Space Complexity

Time Complexity: O(1) for the shouldPrintMessage method

  • The dictionary get operation takes O(1) average time
  • The dictionary assignment operation takes O(1) average time
  • The comparison operation takes O(1) time
  • All operations in the method are constant time, resulting in overall O(1) time complexity per call

Space Complexity: O(M) where M is the number of unique messages

  • The limiter dictionary stores a timestamp for each unique message encountered
  • In the worst case, if all messages are unique, the space grows linearly with the number of unique messages
  • Each entry in the dictionary stores a message string as key and an integer timestamp as value
  • The space complexity could also be expressed as O(N) where N is the total number of unique message strings stored over the lifetime of the Logger object

Common Pitfalls

1. Memory Leak with Unbounded Hash Map Growth

The Problem: The hash map grows indefinitely as new unique messages arrive, but old entries are never removed even when they're no longer relevant. In a production system running for extended periods, this could lead to memory exhaustion.

Example Scenario:

  • System receives 1 million unique messages in the first hour
  • After 10 seconds, these messages are eligible for reprinting but may never appear again
  • The hash map still holds all 1 million entries indefinitely

Solution: Implement periodic cleanup or use a sliding window approach:

class Logger:
    def __init__(self):
        self.message_limiter = {}
        self.cleanup_interval = 100  # Clean up every 100 calls
        self.call_count = 0
  
    def shouldPrintMessage(self, timestamp: int, message: str) -> bool:
        # Periodic cleanup
        self.call_count += 1
        if self.call_count % self.cleanup_interval == 0:
            self._cleanup(timestamp)
      
        next_allowed_time = self.message_limiter.get(message, 0)
      
        if next_allowed_time > timestamp:
            return False
      
        self.message_limiter[message] = timestamp + 10
        return True
  
    def _cleanup(self, current_timestamp):
        # Remove entries that are expired (more than 10 seconds old)
        expired_messages = [msg for msg, time in self.message_limiter.items() 
                           if time <= current_timestamp]
        for msg in expired_messages:
            del self.message_limiter[msg]

2. Incorrect Handling of Edge Case: Same Timestamp

The Problem: Multiple identical messages arriving at the exact same timestamp might not be handled correctly if the logic isn't clear about whether only the first one should print.

Example:

  • Message "foo" arrives at timestamp 5 (first occurrence) → should print
  • Message "foo" arrives again at timestamp 5 → should NOT print

Solution: The current implementation handles this correctly, but ensure you understand why:

  • First "foo" at t=5: Gets printed and sets limiter["foo"] = 15
  • Second "foo" at t=5: Checks if 15 > 5 (true), so returns False

3. Integer Overflow in Long-Running Systems

The Problem: In systems running for very long periods, timestamp values could approach integer limits. Adding 10 to a near-maximum timestamp could cause overflow.

Solution: Use appropriate data types or implement overflow checking:

def shouldPrintMessage(self, timestamp: int, message: str) -> bool:
    next_allowed_time = self.message_limiter.get(message, 0)
  
    if next_allowed_time > timestamp:
        return False
  
    # Check for potential overflow
    import sys
    if timestamp > sys.maxsize - 10:
        # Handle overflow case - could reset or use modular arithmetic
        self.message_limiter[message] = 10  # Reset to small value
    else:
        self.message_limiter[message] = timestamp + 10
  
    return True

4. Concurrency Issues in Multi-threaded Environments

The Problem: If multiple threads call shouldPrintMessage simultaneously with the same message, race conditions could allow duplicate printing within the 10-second window.

Solution: Add thread synchronization:

import threading

class Logger:
    def __init__(self):
        self.message_limiter = {}
        self.lock = threading.Lock()
  
    def shouldPrintMessage(self, timestamp: int, message: str) -> bool:
        with self.lock:
            next_allowed_time = self.message_limiter.get(message, 0)
          
            if next_allowed_time > timestamp:
                return False
          
            self.message_limiter[message] = timestamp + 10
            return True
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

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

Load More