Q6 - The Flapping Alert Filter
We believe this alert-filtering question, and close variations of it, have been reported in recent Google interviews. It opens as one of the friendliest questions you can be handed: hide the repeats in a noisy stream. Most candidates have it working in ten minutes.
Then the interviewer changes one word in the rule, and the whole thing turns inside out. That second version is the actual interview, and it breaks the assumption almost every streaming filter is built on: that you can decide what to do with a record at the moment it arrives.
Scenario: Google runs a monitoring pipeline over thousands of production services. Probes fire short alert lines into a console that an on-call engineer is watching during an incident. Every alert carries two things: the second it fired, and the alert text.
They arrive one at a time, oldest first, and look like this:
time:1 disk-full
time:5 disk-full
time:12 api-5xx
Part 1: The console is drowning in repeats. Build a filter that shows an alert only if that same alert text has not already been shown in the last 10 seconds. Anything repeating inside that cooldown is dropped.
🧩 Settle the fuzzy parts out loud before you type. This prompt leaves several things unsaid on purpose, and naming them is part of what is being graded. Worth asking: is this a live stream or a batch you get up front? Can two alerts share the exact same timestamp? Is the boundary inclusive, so are two copies exactly 10 seconds apart a repeat or not? Is the text matched exactly, or normalised first? And the sharpest one for Part 1: does the cooldown run from the last time this alert was shown, or the last time it was seen? Those are different filters, and picking one out loud costs you five seconds and buys you a lot of credit. You have about 45 minutes, so pin the rules down, then commit.
For Part 1 we take the usual reading: the cooldown runs from the last copy we actually showed, and the stream arrives in non-decreasing time order.
Example 1: the cooldown filter
ALERTS (arrival order):
time:1 disk-full
time:5 disk-full
time:12 disk-full
time:23 disk-full
CONSOLE (Part 1 rule, 10s cooldown from the last one SHOWN):
[1] disk-full first sighting, nothing to compare against
(5 dropped) only 4s after the copy shown at 1
[12] disk-full 11s after the copy shown at 1, cooldown expired
[23] disk-full 11s after the copy shown at 12, cooldown expired
Part 1: The Cooldown Filter
Strip the story away and almost nothing is left. For each alert text you need one fact: the last time you showed it. That is a map from text to timestamp.
When an alert arrives, look its text up. If you have never shown it, or the gap since you last showed it is at least 10 seconds, show it and record the new time. Otherwise drop it and change nothing. One lookup, one comparison, one write.
The detail worth saying out loud is the one the cooldown hangs on: you record the time you showed a copy, not the time you saw one. A burst of copies arriving every two seconds forever will keep failing the check against that same stored time, so the alert surfaces once every 10 seconds instead of never surfacing again.
class AlertConsole:
"""Part 1: show an alert unless the same text was shown in the last 10 seconds."""
def __init__(self, window_seconds=10):
self.window_seconds = window_seconds
self.last_shown = {} # alert text -> when we last showed it
def receive(self, timestamp, text):
last = self.last_shown.get(text)
if last is not None and timestamp - last < self.window_seconds:
return False # still inside the cooldown, drop it
self.last_shown[text] = timestamp
return True # show it
⏱️ Google builds these up as you go. They rarely hand you every part at once. You get the simple version working, and then they add a wrinkle that makes you rethink the structure you just committed to. So keep Part 1 tidy and easy to extend, and do not spend so long polishing it that you meet the real question with ten minutes left.
Part 2: When a Repeat Poisons Both
🗣️ This part is hard, and not finishing it is normal. Landing Part 2 cleanly on top of Part 1 is a lot to fit into 45 minutes. Candidates report that walking the interviewer through the design out loud, the buffer, the reach-back, and the moment you commit, is often enough to move to the next round even when the code is unfinished. Make sure they can see you would get there with a little more time.
Here is the wrinkle. The on-call engineer comes back unhappy. A flapping alert, one that fires, clears, and fires again seconds later, is almost always noise: a probe bouncing, a pod restarting, a threshold sitting right on the line. Showing the first copy still put a false alarm on their screen at 3am.
So the rule changes, and only one word really moves:
An alert is shown only if no other copy of the same text fired within 10 seconds of it. If two copies land within 10 seconds of each other, that alert is flapping, and both copies are hidden.
Read that twice, because it breaks something structural. Under Part 1 you could decide an alert's fate the moment it arrived. Under this rule you cannot. The alert at time 1 looks perfectly isolated when it lands. Four seconds later a second copy shows up, and now the first one is noise too, except you already printed it to a console you cannot un-print.
Run the exact same stream from Example 1 through the new rule and watch how much changes.
Example 2: same stream as Example 1, one word different in the rule
ALERTS (arrival order):
time:1 disk-full
time:5 disk-full
time:12 disk-full
time:23 disk-full
VERDICT per alert:
[1] hidden a copy fires at 5, which is 4s away (4 < 10)
[5] hidden within 10s of BOTH the copy at 1 and the copy at 12
[12] hidden a copy fired at 5, which is 7s away (7 < 10)
[23] SHOWN nearest copy is at 12, which is 11s away (11 >= 10)
CONSOLE:
[23] disk-full
Part 1 showed three of these four. Part 2 shows one.
Two copies can take each other down even when neither is near anything else, and a middle copy can be poisoned from both sides at once. Note also that 12 and 23 are 11 seconds apart, so that pair is fine; 12 dies because of 5, not because of 23.
Example 3: different texts never interfere, and survivors do exist
ALERTS (arrival order):
time:2 cpu-hot
time:3 disk-full
time:9 cpu-hot <-- 7s after cpu-hot@2
time:20 disk-full <-- 17s after disk-full@3
VERDICT per alert:
[2] hidden cpu-hot fires again at 9 (7 < 10)
[9] hidden the other half of that same flapping pair
[3] SHOWN nearest disk-full is at 20 (17 >= 10)
[20] SHOWN nearest disk-full is at 3 (17 >= 10)
CONSOLE (in time order):
[3] disk-full
[20] disk-full
cpu-hot and disk-full are compared only against themselves, so a busy text never drags a quiet one down with it. Each alert text gets its own independent timeline.
Step 1: You cannot print on arrival, so buffer instead
The first move is to stop trying to answer immediately. When an alert lands you do not know enough yet, so hold it and write down a provisional verdict you are allowed to revise.
Give every buffered alert one mutable flag, suppressed, starting at false. A later arrival is allowed to reach back and flip that flag to true. Nothing is printed while an alert sits in the buffer.
buffer after time:1 (nothing printed yet)
[ 1 disk-full suppressed=false ]
buffer after time:5 5 - 1 = 4, under 10, so the pair is flapping
[ 1 disk-full suppressed=TRUE ] <-- reached back and flipped
[ 5 disk-full suppressed=TRUE ]
Step 2: Only the previous copy of that text can ever matter
The obvious way to check a new alert is to compare it against every copy of that text you have ever seen. That is O(k) per alert and it keeps getting slower as the stream runs. You can do far better, and the reason is worth saying out loud in the room.
Because the stream arrives in time order, take any three copies of the same text at times a < b < c. If a and c are within 10 seconds of each other, then b sits between them, so b is within 10 seconds of a too.
a ............ b ...... c
|<------- under 10s ------>|
|<-- under 10s -->| b is closer to a than c is
So if any pair of copies is close enough to matter, then some adjacent pair is already close enough. The consequence: you never need to look further back than the single most recent copy of that exact text. One lookup, not a scan.
Step 3: An alert is final once the stream passes its window
Buffered alerts cannot sit there forever, so you need the moment it becomes safe to commit one.
An alert at time t can only be killed by a copy that fires within 10 seconds of it. Once the stream's clock reaches t + 10, every future arrival is stamped t + 10 or later, which is at least 10 seconds away from t. Nothing that can still arrive is able to touch it. Its verdict is final.
So on each arrival at time now, first settle the front of the buffer: while the oldest buffered alert has timestamp + 10 <= now, pop it and print it if its flag is still false. Because arrivals come in time order, the buffer is already sorted by timestamp, so a plain queue works and the front is always the next one to close.
That gives the whole receive method: settle what has closed, look up the one previous copy, flip both flags if they are too close, then buffer this one.
def receive(self, timestamp, text):
self._settle_through(timestamp) # commit everything whose window closed
previous = self.open_alert.get(text) # the one copy that could still clash
alert = {"timestamp": timestamp, "text": text, "suppressed": False}
if previous is not None and timestamp - previous["timestamp"] < self.window_seconds:
previous["suppressed"] = True # reach back and kill the earlier copy
alert["suppressed"] = True # and this one is noise too
self.pending.append(alert)
self.open_alert[text] = alert
One case is easy to miss: when the stream ends, alerts whose windows never closed are still sitting in the buffer. Nothing more can arrive to kill them, so a final flush prints every survivor. Forget it and your last few seconds of output silently vanish.
Watch Example 2 run frame by frame. Each bar is one alert's 10-second window, and the dashed line is where the stream has reached, so a clash is simply the line landing inside an open bar. The last two frames cover the two ideas underneath it:
Part 3: Keep It Bounded, the Stream Never Ends
If your interviewer likes what they see, the next question is usually about memory. This console runs for months. What in your design grows without limit?
The buffer is fine: alerts leave it as soon as their windows close. The lookup map is the problem. Key it by alert text and never remove anything, and you are holding one entry for every distinct alert text the system has ever emitted, forever, long after those alerts stopped mattering.
The fix falls out of Step 3. Once an alert's window has closed, that copy can never clash with anything that arrives later, which is exactly why you were able to settle it. So it has no business staying in the lookup map either. Prune it at the same moment, with one guard: only remove the entry if it still points at the copy you are settling. If a newer copy of that text has already replaced it, that newer copy still has an open window and must stay.
alert = self.pending.popleft()
if not alert["suppressed"]:
ready.append(alert)
# the window has closed, so this copy can never clash again: stop tracking it,
# unless a newer copy of the same text has already taken its place
if self.open_alert.get(alert["text"]) is alert:
del self.open_alert[alert["text"]]
Now both structures hold only what is still inside an open window. Memory tracks how many alerts arrive in any 10 seconds, not how long the process has been up. Say that part out loud: interviewers ask the memory question specifically to find out whether you think past the happy path.
The Full Solution
Putting Parts 2 and 3 together. receive hands back the alerts that just became final, which is usually nothing, and endOfStream flushes the survivors when the feed stops.
from collections import deque
class FlappingAlertFilter:
"""Show an alert only if no other copy of the same text fired within the window."""
def __init__(self, window_seconds=10):
self.window_seconds = window_seconds
self.pending = deque() # alerts with open windows, oldest first
self.open_alert = {} # alert text -> its most recent open copy
def receive(self, timestamp, text):
"""Take one alert from the stream, return any alerts that are now final."""
ready = self._settle_through(timestamp)
previous = self.open_alert.get(text)
alert = {"timestamp": timestamp, "text": text, "suppressed": False}
if previous is not None and timestamp - previous["timestamp"] < self.window_seconds:
previous["suppressed"] = True # reach back: the earlier copy is noise too
alert["suppressed"] = True
self.pending.append(alert)
self.open_alert[text] = alert
return ready
def _settle_through(self, now):
"""Commit every alert whose 10-second window has closed by `now`."""
ready = []
while self.pending and self.pending[0]["timestamp"] + self.window_seconds <= now:
alert = self.pending.popleft()
if not alert["suppressed"]:
ready.append(alert)
if self.open_alert.get(alert["text"]) is alert:
del self.open_alert[alert["text"]] # closed window: stop tracking it
return ready
def end_of_stream(self):
"""Nothing else can arrive, so every survivor still buffered is final."""
ready = [alert for alert in self.pending if not alert["suppressed"]]
self.pending.clear()
self.open_alert.clear()
return ready
Complexity Analysis
receive: amortizedO(1). One map lookup, one flag write, one append. A single call can settle several buffered alerts at once, but every alert is buffered exactly once and settled exactly once, so a stream ofnalerts costsO(n)in total.- Memory:
O(open alerts), meaning the alerts whose 10-second windows have not closed yet. That is set by the arrival rate in any 10-second span, not by how long the process has been running, and the pruning in Part 3 is what keeps it there. - Output delay: up to the window. An alert is printed no earlier than 10 seconds after it fires, which is the unavoidable price of a rule that lets the future change the past. Name this trade-off in the room before the interviewer does.
The naive version is worth saying out loud so they know you can see it: store every arrival per text, and on each new alert scan that text's history for anything within 10 seconds. It gives the right answer, but it is O(k) per alert and degrades toward O(n^2), and the history grows forever. Both problems disappear once you notice you only ever need the previous copy and only until its window shuts.
Final Notes
We believe this flapping-alert question, and close variations of it, have appeared frequently in recent Google interviews. It tests something a standard filtering question cannot: what you do when a record's fate is not knowable at the moment it arrives, and your design has to hold a decision open instead of committing to it.
- The one-word change is the entire interview. Part 1 hides the repeat. Part 2 hides the repeat and the original. That difference is what removes your ability to answer on arrival, and candidates who skim the new rule write Part 1 twice and never notice.
- Buffer, then settle. When the future can invalidate the past, hold each record with a revisable verdict and commit it only once nothing can still change it, which here is the moment the stream's clock passes
t + 10. The queue is already in time order because the arrivals are, so the front is always the next one to close. - Only the previous copy of the same text can matter. Three copies at
a < b < cwithaandcwithin 10 seconds meansbis within 10 seconds ofaas well, so a close pair is always an adjacent pair. That single observation turns a growing scan into one lookup. - Do not lose the tail. Alerts whose windows never close are still buffered when the feed stops, and they are all survivors. A stream with no flush silently drops its last few seconds, which is the bug most likely to surface in the room.
- Answer the memory question before it is asked. A correct solution can still hold one key for every alert text the system has ever emitted, which is an unbounded map. Drop each entry the moment its window closes, guarding against a newer copy that has already replaced it, and memory stays proportional to one window.
The reason Google keeps reaching for questions shaped like this is that production streams really do behave this way: the thing you need in order to judge a record often shows up after the record does. Get comfortable saying "I cannot decide this yet, so here is where I will hold it and here is exactly when I will commit," and you are most of the way through the round. 🚀