1268. Logger Rate Limiter
easyAsked at ShopifyShopify's lightest rate-limiting design. Real motivation: throttle warning logs from a misbehaving merchant integration so the log pipeline doesn't get DOSed. The interviewer wants to see a hash-map design and a clear answer on memory growth.
By Alex Chen, Founder, InterviewChamp.AI · Last verified
Source citations
Public interview reports confirming this problem appears in Shopify loops.
- Glassdoor (2026-Q1)— Shopify Production Engineer + Backend Developer onsites cite the Logger Rate Limiter / token-bucket pattern as a system-design-lite warm-up.
- Reddit r/cscareerquestions (2025-12)— Shopify SRE-track interview reports include this with the distributed rate-limiter follow-up.
Problem
Design a logger system that receives a stream of messages along with their timestamps. Each unique message should only be printed at most every 10 seconds. Given a message and a timestamp (in seconds granularity), return true if the message should be printed in the given timestamp, otherwise returns false.
Constraints
0 <= timestamp <= 10^9Every timestamp will be passed in non-decreasing order (chronological order).1 <= message.length <= 30At most 10^4 calls will be made to shouldPrintMessage.
Examples
Example 1
shouldPrintMessage(1, "foo"); shouldPrintMessage(2, "bar"); shouldPrintMessage(3, "foo"); shouldPrintMessage(8, "bar"); shouldPrintMessage(10, "foo"); shouldPrintMessage(11, "foo")[true,true,false,false,false,true]Approaches
1. Hash map with unbounded growth
Store the last-printed timestamp per message. On call, check if (timestamp - lastSeen) >= 10.
- Time
- O(1) per call
- Space
- O(unique messages, unbounded)
class LoggerUnbounded {
constructor() { this.lastPrinted = new Map(); }
shouldPrintMessage(timestamp, message) {
if (!this.lastPrinted.has(message) || timestamp - this.lastPrinted.get(message) >= 10) {
this.lastPrinted.set(message, timestamp);
return true;
}
return false;
}
}Tradeoff: Easy and correct. Memory grows linearly with unique messages — at Shopify's scale, that's the smell. Mention it; volunteer the bounded variant unprompted.
2. Hash map + lazy eviction (optimal)
Same logic, but on each call, opportunistically prune entries older than (timestamp - 10). For chronological inputs, this caps memory at the number of distinct messages in any 10-second window.
- Time
- O(1) amortized per call
- Space
- O(messages in last 10s)
class Logger {
constructor() { this.lastPrinted = new Map(); }
shouldPrintMessage(timestamp, message) {
// Lazy eviction every call adds O(N); do it periodically instead.
if (this.lastPrinted.size > 1000) {
const cutoff = timestamp - 10;
for (const [msg, t] of this.lastPrinted) {
if (t < cutoff) this.lastPrinted.delete(msg);
}
}
const last = this.lastPrinted.get(message);
if (last === undefined || timestamp - last >= 10) {
this.lastPrinted.set(message, timestamp);
return true;
}
return false;
}
}Tradeoff: Bounds memory at the cost of an occasional sweep. The threshold (1000) is tunable; pick something that amortizes the sweep to O(1) per call. Shopify accepts this if you discuss the size/sweep tradeoff.
3. Queue + set (alternative bounded design)
Maintain a queue of (timestamp, message) for the last 10 seconds + a Set of currently throttled messages. On each call, pop expired entries from the queue and remove from the Set.
- Time
- O(1) amortized per call
- Space
- O(messages in last 10s)
class LoggerQueue {
constructor() {
this.queue = [];
this.active = new Set();
}
shouldPrintMessage(timestamp, message) {
while (this.queue.length && this.queue[0].t <= timestamp - 10) {
this.active.delete(this.queue.shift().msg);
}
if (this.active.has(message)) return false;
this.active.add(message);
this.queue.push({ t: timestamp, msg: message });
return true;
}
}Tradeoff: Memory strictly bounded by the 10-second window. Slightly more code than the hash-map version. Array.shift is O(n); for high QPS, switch to a head-pointer deque.
Shopify-specific tips
Shopify's expected follow-up is 'how do you scale this across multiple servers' — answer: shard by message hash; or accept best-effort throttling (each server enforces locally, accepting some over-print). Senior candidates are also asked to extend to a token bucket: 'now allow K prints per 10 seconds, not just one' — store an array of recent timestamps per message.
Common mistakes
- Using > instead of >= when comparing the cooldown — off-by-one on the boundary.
- Forgetting to update lastPrinted when returning true (the next call will print again immediately).
- Letting the hash map grow without bound — interviewers always probe this.
- Assuming the timestamp will not have decimals (LeetCode's signature has integer seconds; real systems use ms or epoch).
Follow-up questions
An interviewer at Shopify may pivot to one of these next:
- Distributed rate limiter — shard by message hash.
- Token bucket — allow K prints per window.
- Sliding window log — more precise than fixed window.
- Approximate counts — Count-Min Sketch for very high cardinality.
- LeetCode 1268 — search suggestions system (rate-limited results).
Solve it now
Free. No sign-up. Python and JavaScript run instantly in your browser.
FAQ
Why use a hash map and not a sorted structure?
Because lookups by message string are the hot path, and a hash map gives O(1). Sorted structures give O(log n) lookups and gain nothing. The only sorted-friendly variant is when you need 'oldest entry first' for eviction, which is what the queue-based design uses.
Is unbounded memory actually a problem in practice?
At Shopify scale, yes. Unique log messages across all merchants can reach millions per minute. The lazy-eviction or queue-based design caps memory at the active window, which is the engineering bar interviewers grade against.
Free learning resources
Curated free links for this problem.
Practice these live with InterviewChamp.AI
Drill Logger Rate Limiter and other Shopify interview questions under real-loop conditions with instant feedback on your reasoning, complexity claims, and code.
Practice these live with InterviewChamp.AI →