362. Design Hit Counter
mediumAsked at TwilioDesign Hit Counter is Twilio's rolling-window probe: track the number of hits received in the past 300 seconds. The grading rubric weighs whether you use a bounded 300-bucket array (memory-efficient regardless of request rate) or a queue (flexible but unbounded under high load), and articulate the tradeoff.
By Alex Chen, Founder, InterviewChamp.AI · Last verified
Source citations
Public interview reports confirming this problem appears in Twilio loops.
- Glassdoor (2026-Q1)— Heavily-cited in Twilio API-platform on-site reports as the immediate follow-up to Logger Rate Limiter.
- LeetCode Discuss (2025-11)— Listed in Twilio interview reports across messaging and analytics teams.
Problem
Design a hit counter which counts the number of hits received in the past 5 minutes (i.e., the past 300 seconds). Your system should accept a timestamp parameter (in seconds granularity), and you may assume that calls are being made to the system in chronological order (i.e., timestamp is monotonically increasing). Several hits may arrive roughly at the same time. Implement the HitCounter class: HitCounter() initializes the object of the hit counter system; void hit(int timestamp) records a hit that happened at timestamp; int getHits(int timestamp) returns the number of hits in the past 5 minutes from timestamp.
Constraints
1 <= timestamp <= 2 * 10^9All the calls are being made to the system in chronological order (i.e., timestamp is monotonically increasing).At most 300 calls will be made to hit and getHits.
Examples
Example 1
hit(1), hit(2), hit(3), getHits(4), hit(300), getHits(300), getHits(301)[null, null, null, 3, null, 4, 3]Approaches
1. Queue of timestamps (canonical)
Push each hit's timestamp into a queue. On getHits, evict timestamps that are older than 300 seconds, return the queue size.
- Time
- hit O(1), getHits amortized O(1)
- Space
- O(hits in last 300s)
class HitCounter {
constructor() {
this.queue = [];
this.head = 0; // index-based to avoid O(n) shift
}
hit(timestamp) {
this.queue.push(timestamp);
}
getHits(timestamp) {
while (this.head < this.queue.length && this.queue[this.head] <= timestamp - 300) {
this.head++;
}
return this.queue.length - this.head;
}
}Tradeoff: Memory grows with hit rate — if you have 10^6 hits in 300 seconds, the queue holds 10^6 entries. Acceptable for the LC contract but problematic at production scale.
2. Fixed 300-bucket circular array (memory-bounded optimal)
Use two arrays of size 300: times[i] is the most recent timestamp seen for bucket i, counts[i] is the count at that timestamp. On hit, overwrite bucket (timestamp % 300) if its time differs. On getHits, sum counts where times[i] > timestamp - 300.
- Time
- hit O(1), getHits O(300) = O(1) bounded
- Space
- O(300) = O(1)
class HitCounterBucketed {
constructor() {
this.times = new Array(300).fill(0);
this.counts = new Array(300).fill(0);
}
hit(timestamp) {
const i = timestamp % 300;
if (this.times[i] === timestamp) {
this.counts[i]++;
} else {
this.times[i] = timestamp;
this.counts[i] = 1;
}
}
getHits(timestamp) {
let total = 0;
for (let i = 0; i < 300; i++) {
if (this.times[i] > timestamp - 300) total += this.counts[i];
}
return total;
}
}Tradeoff: O(1) memory regardless of request rate — the production answer. Each bucket holds (last-seen-timestamp, count-at-that-timestamp); the mod-by-300 wraparound is the entire trick. Twilio interviewers strongly prefer this version for the follow-up: 'what if hits per second hit 10^6?'
Twilio-specific tips
Twilio's API-platform team uses this question to probe the same rolling-window pattern as their production sliding-window-counter rate limiter. The bar is the bounded-bucket version: state up front that the queue grows with hit rate, then derive the 300-bucket version. The wraparound check (`times[i] === timestamp`) is what makes this safe across multiple seconds — without it, two hits at t=1 and t=301 would both increment bucket 1.
Common mistakes
- Forgetting the wraparound timestamp check in the bucketed version — counts from minutes ago leak into the current window.
- Using `timestamp - 300 <= time` (inclusive) instead of strict `<` — hits at exactly t - 300 should NOT count for time t (300 seconds is exclusive).
- In the queue version, using array.shift() instead of an index pointer — O(n) per shift kills the amortized O(1) guarantee.
Follow-up questions
An interviewer at Twilio may pivot to one of these next:
- Generalize to arbitrary window W. (Same bucketed approach with W buckets — memory is O(W) regardless of hit rate.)
- What if hit() is called at sub-second granularity? (Two-level bucketing: 300 second-buckets, each containing a small subsecond array.)
- How would you distribute this across N nodes? (Per-node local counter + periodic merge to a central aggregator, or HyperLogLog for approximate distinct counts.)
Solve it now
Free. No sign-up. Python and JavaScript run instantly in your browser.
FAQ
Why doesn't the queue version work for production?
Because memory is unbounded — if your service receives 10^6 hits per second, the queue holds 3 * 10^8 entries in the active window. The bucketed version uses 300 entries regardless of hit rate.
What does the `times[i] === timestamp` check protect against?
Multiple hits at the same timestamp incrementing the same bucket vs. a fresh second writing into a bucket from 5+ minutes ago. The check 'same second? add 1; different second? reset and start at 1' is the correctness invariant.
Free learning resources
Curated free links for this problem.
Practice these live with InterviewChamp.AI
Drill Design Hit Counter and other Twilio interview questions under real-loop conditions with instant feedback on your reasoning, complexity claims, and code.
Practice these live with InterviewChamp.AI →