Skip to content

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., your practice guide

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 install

Tier 2 · Web · paste-in setup · No account

Version tested: Python 3.12.7 / Pyodide 0.27.6 in JupyterLite 0.6.4; Chromium 148.0.7778.96; captured against a local Mosquitto-compatible WebSocket broker because the capture host cannot reach the public broker; the notebook targets test.mosquitto.org; captureSource playwright:jupyterlite. Date: 2026-09-09.

Open 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.

Open in your browser (new tab)

Steps

Screens captured against JupyterLite Python 3.12.7 / Pyodide 0.27.6; Chromium 148.0.7778.96 on 2026-09-10; the tool may have moved on — the text steps are the contract.

  1. 1 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 1 and 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: Under Event contract, the field table shows source freezer-7, event_second reading time in seconds, celsius temperature in Celsius, and status 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.
    JupyterLite notebook output for mqtt-windowed-alerts step 1, captured after running the real Python cell.
    Step 1 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  2. 2 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, six topic payload lines show r1 through r6 arriving before Delivery 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.
    JupyterLite notebook output for mqtt-windowed-alerts step 2, captured after running the real Python cell.
    Step 2 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  3. 3 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 3 to 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: The arrival buffer after arrival table grows from r1 r1 to r6 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.
    JupyterLite notebook output for mqtt-windowed-alerts step 3, captured after running the real Python cell.
    Step 3 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  4. 4 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 4 to 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 places r1@50 r2@52 r3@54 in [0, 60) and r4@60 r5@62 r6@64 in [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.
    JupyterLite notebook output for mqtt-windowed-alerts step 4, captured after running the real Python cell.
    Step 4 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  5. 5 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 5 to 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 r6 YES, followed by Rule: 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.
    JupyterLite notebook output for mqtt-windowed-alerts step 5, captured after running the real Python cell.
    Step 5 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  6. 6 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 6 to 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 reads 9 OFF, 11 ON, 10 ON, 9 ON, 8 ON, and 6 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.
    JupyterLite notebook output for mqtt-windowed-alerts step 6, captured after running the real Python cell.
    Step 6 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  7. 7 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 7 to 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 shows warm-run-42 EMIT alert once and warm-run-42 SUPPRESS duplicate twice 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.
    JupyterLite notebook output for mqtt-windowed-alerts step 7, captured after running the real Python cell.
    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.

  1. What makes an IoT streaming pipeline reviewable before downstream consumers depend on it?

    Return to the chapter’s knowledge check
  2. After 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

Caution

The public broker is shared and offers no privacy or availability guarantee. Publish only the fictional readings generated here, never credentials or personal data. Topic randomness reduces accidental cross-talk but is not access control; use an authenticated private broker for real devices.

Return to Building IoT Streaming Pipelines · Browse Labs