Build stable windowed alerts from an MQTT stream
Publish a small sensor stream, expose a tumbling-window blind spot, and produce one stable deduplicated alert from the same evidence.

Data Dora: I want you to trace every alert back to its MQTT event IDs, window rule, and output guard.
Predict the reading, then compare it with the measurement.
Python 3 in your browser (JupyterLite)
Python · no installOpen the notebook in your browser and run each Python cell; no install or account is needed.
Three ways to run: use JupyterLite here with no install; run main.py locally from the downloadable lab folder; or open the same notebook in Google Colab.
Steps
Step 1
- Do
- In the JupyterLite notebook, run the Step 1 cell to inspect the event contract before processing any reading.
- You will see
- Input: Run
python3 main.py --step 1and inspect the event contract before processing any reading. Observed heading: "Event contract" Observed intermediate: "event_second reading time in seconds" Observed rule or check: "status valid" Observed result: "STEP 1 event-contract source=freezer-7 unit=celsius time=event-second status=valid" Expected from the existing capture: UnderEvent contract, the field table showssource freezer-7,event_second reading time in seconds,celsius temperature in Celsius, andstatus valid. - Why it matters
- The table makes the source, clock, measurement unit, and status semantics inspectable before any processing decision uses them. Intermediate evidence: "event_second reading time in seconds" gives a value or state to inspect. Comparison cue: "status valid" supplies the rule or adjacent evidence for that check. Result: "STEP 1 event-contract source=freezer-7 unit=celsius time=event-second status=valid" records how this input was classified.

Step 1 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 2
- Do
- In the JupyterLite notebook, run the Step 2 cell to publish, while connected to the internet, six fictional readings to its random MQTT topic.
- You will see
- Under
MQTT messages received, sixtopic payloadlines show r1 through r6 arriving beforeDelivery check: every published event ID returned to this subscriber. - Why it matters
- Seeing each broker-delivered topic and JSON payload lets the learner verify the source-to-intake handoff behind the final six-of-six count.

Step 2 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 3
- Do
- In the JupyterLite notebook, run the Step 3 cell to place all six event IDs into a four-entry recent-history buffer.
- You will see
- Input: Run
python3 main.py --step 3to place all six event IDs into a four-entry recent-history buffer. Observed heading: "Bounded recent-history buffer" Observed intermediate: "r4 r1 r2 r3 r4" Observed rule or check: "Eviction rule: discard the oldest ID when capacity 4 is exceeded." Observed result: "STEP 3 bounded-buffer capacity=4 retained=r3,r4,r5,r6 evicted=r1,r2" Expected from the existing capture: Thearrival buffer after arrivaltable grows fromr1 r1tor6 r3 r4 r5 r6, followed by the oldest-ID eviction rule. - Why it matters
- The row-by-row state trace shows exactly when r1 and r2 leave capacity-four state, distinguishing intentional eviction from loss. Intermediate evidence: "r4 r1 r2 r3 r4" gives a value or state to inspect. Comparison cue: "Eviction rule: discard the oldest ID when capacity 4 is exceeded." supplies the rule or adjacent evidence for that check. Result: "STEP 3 bounded-buffer capacity=4 retained=r3,r4,r5,r6 evicted=r1,r2" records how this input was classified.

Step 3 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 4
- Do
- In the JupyterLite notebook, run the Step 4 cell to count the six-event burst in adjacent 60-second tumbling windows.
- You will see
- Input: Run
python3 main.py --step 4to count the six-event burst in adjacent 60-second tumbling windows. Observed heading: "Tumbling windows: fixed, non-overlapping 60-second boundaries" Observed intermediate: "[60, 120) r4@60 r5@62 r6@64" Observed rule or check: "Decision: 3 and 3 are both below 6; boundary split hides the burst." Observed result: "STEP 4 tumbling-60s counts=3,3 threshold=6 alert=OFF boundary-split=yes" Expected from the existing capture: The window table placesr1@50 r2@52 r3@54in[0, 60)andr4@60 r5@62 r6@64in[60, 120), then says the 3-and-3 split hides the burst. - Why it matters
- Showing boundaries and members proves why neither fixed window reaches six, rather than asking the summary count to explain itself. Intermediate evidence: "[60, 120) r4@60 r5@62 r6@64" gives a value or state to inspect. Comparison cue: "Decision: 3 and 3 are both below 6; boundary split hides the burst." supplies the rule or adjacent evidence for that check. Result: "STEP 4 tumbling-60s counts=3,3 threshold=6 alert=OFF boundary-split=yes" records how this input was classified.

Step 4 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 5
- Do
- In the JupyterLite notebook, run the Step 5 cell to evaluate a 60-second sliding window beginning at event-second 50.
- You will see
- Input: Run
python3 main.py --step 5to evaluate a 60-second sliding window beginning at event-second 50. Observed heading: "Sliding window: start 50 seconds, end 110 seconds" Observed intermediate: "r4 60 YES" Observed rule or check: "Rule: alert when the window contains at least 6 readings." Observed result: "STEP 5 sliding-60s start=50 count=6 threshold=6 alert=ON ids=r1,r2,r3,r4,r5,r6" Expected from the existing capture: The[50, 110)membership table marks every reading r1 through r6YES, followed byRule: alert when the window contains at least 6 readings. - Why it matters
- The explicit boundary and six member rows let the learner recompute the alert and compare sliding-window behavior with the split tumbling windows. Intermediate evidence: "r4 60 YES" gives a value or state to inspect. Comparison cue: "Rule: alert when the window contains at least 6 readings." supplies the rule or adjacent evidence for that check. Result: "STEP 5 sliding-60s start=50 count=6 threshold=6 alert=ON ids=r1,r2,r3,r4,r5,r6" records how this input was classified.

Step 5 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 6
- Do
- In the JupyterLite notebook, run the Step 6 cell to pass counts 9, 11, 10, 9, 8, and 6 through separate on and clear thresholds.
- You will see
- Input: Run
python3 main.py --step 6to pass counts 9, 11, 10, 9, 8, and 6 through separate on and clear thresholds. Observed heading: "Hysteresis state trace" Observed intermediate: "9 ON" Observed rule or check: "Rules: turn ON above 10; clear only below 7." Observed result: "STEP 6 hysteresis on=>10 clear=<7 transitions=11:ON,6:OFF final=OFF" Expected from the existing capture: The state trace reads9 OFF,11 ON,10 ON,9 ON,8 ON, and6 OFF, then names the turn-on and clear rules. - Why it matters
- Following every input through both thresholds shows how hysteresis holds state through intermediate counts instead of merely reporting two transitions. Intermediate evidence: "9 ON" gives a value or state to inspect. Comparison cue: "Rules: turn ON above 10; clear only below 7." supplies the rule or adjacent evidence for that check. Result: "STEP 6 hysteresis on=>10 clear=<7 transitions=11:ON,6:OFF final=OFF" records how this input was classified.

Step 6 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) Step 7
- Do
- In the JupyterLite notebook, run the Step 7 cell to submit the same warm-run event ID three times to the output guard.
- You will see
- Input: Run
python3 main.py --step 7to submit the same warm-run event ID three times to the output guard. Observed heading: "Deduplication output guard" Observed intermediate: "warm-run-42 SUPPRESS duplicate" Observed rule or check: "Rule: emit at most once for each stable event ID." Observed result: "STEP 7 dedupe key=warm-run-42 candidates=3 emitted=1 suppressed=2" Expected from the existing capture: The decision table showswarm-run-42 EMIT alertonce andwarm-run-42 SUPPRESS duplicatetwice before naming the one-per-ID rule. - Why it matters
- Showing each candidate and guard decision makes the idempotent side effect auditable across repeated delivery. Intermediate evidence: "warm-run-42 SUPPRESS duplicate" gives a value or state to inspect. Comparison cue: "Rule: emit at most once for each stable event ID." supplies the rule or adjacent evidence for that check. Result: "STEP 7 dedupe key=warm-run-42 candidates=3 emitted=1 suppressed=2" records how this input was classified.

Step 7 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
Chapter checks
These questions refer to the chapter’s examples. Use the return links to review their answers.
What makes an IoT streaming pipeline reviewable before downstream consumers depend on it?
Return to the chapter’s knowledge checkAfter a processor restart, replayed events produce duplicate alerts for the same equipment event. Which part of the pipeline review should be investigated first?
Return to the chapter’s knowledge check