Skip to content

Store and retain MQTT time-series data in SQLite

Ingest a small MQTT stream into SQLite and preserve schema, query, late-data, retention, and restore evidence without Docker or a cloud account.

Data Dora: I want you to prove what arrived, what was stored, what a rollup hides, and what must be restored before deletion., your practice guide

Data Dora: I want you to prove what arrived, what was stored, what a rollup hides, and what must be restored before deletion.
Predict the reading, then compare it with the measurement.

Python 3 in your browser (JupyterLite)

Python · no install

Ingest a small MQTT stream into SQLite and preserve schema, query, late-data, retention, and restore evidence without Docker or a cloud account.

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
    Run `python3 main.py --step 1` to create the in-memory readings table and inspect its index contract.
    You will see
    The terminal prints the `CREATE TABLE` and `CREATE INDEX` SQL, then the PRAGMA rows `0 sensor` and `1 observed_at`.
    Why it matters
    Seeing both schema statements and SQLite's returned index metadata makes identity and observed-time access reviewable before data arrives.
    JupyterLite notebook output for mqtt-sqlite-time-series 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
    Run `python3 main.py --step 2` online to publish six fictional readings to two sensor topics on the public broker.
    You will see
    Six `topic payload` lines show r1 through r6 arriving on living-room and kitchen topics before `Delivery check: six validated payloads reached the subscriber.`
    Why it matters
    The actual received topics and JSON establish which sensor identities and fields reached intake behind the final delivery count.
    JupyterLite notebook output for mqtt-sqlite-time-series 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
    Run `python3 main.py --step 3` to validate a payload after deliberately removing its humidity field.
    You will see
    The displayed rejected JSON lacks humidity beside `Rule: every Reading field is required before storage.` and `Broken rule: required field check -> missing=humidity`.
    Why it matters
    Putting the malformed payload next to the violated rule lets the learner inspect why rejection is deliberate rather than a silent storage gap.
    JupyterLite notebook output for mqtt-sqlite-time-series 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
    Run `python3 main.py --step 4` to insert the validated fixture into SQLite.
    You will see
    After the printed SELECT statement, a six-row table lists each event, sensor, observed time, and temperature; the duplicate rule names event_id as primary key.
    Why it matters
    The returned rows prove what was stored, while the identity rule explains how a replayed event would be constrained.
    JupyterLite notebook output for mqtt-sqlite-time-series 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
    Run `python3 main.py --step 5` to query only the living-room sensor window.
    You will see
    The detail SELECT returns r1=20.0, r2=21.0, and r3=22.0; the aggregate SELECT then returns `3 21.0 20.0 22.0`.
    Why it matters
    Showing both SQL statements, source rows, and aggregate row lets the learner recompute the rollup and see what its average hides.
    JupyterLite notebook output for mqtt-sqlite-time-series 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
    Run `python3 main.py --step 6` to add one reading whose observed time belongs inside the earlier window but whose receive time is later.
    You will see
    The late-1 row shows observed_at=105 and received_at=205 beside a before/after table changing 3 at 21.0 to 4 at 21.8.
    Why it matters
    The late row and repeated aggregate expose why the result changes and why consumers must receive the CORRECTED label.
    JupyterLite notebook output for mqtt-sqlite-time-series 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
    Run `python3 main.py --step 7` to dry-run deletion of readings older than the cutoff; the transaction is rolled back.
    You will see
    The retention SELECT lists r1, r4, r2, and r5 as candidates, followed by the DELETE SQL and `Transaction action: ROLLBACK; no candidate was deleted.`
    Why it matters
    Showing candidate rows before the destructive statement lets the learner audit cutoff scope and confirm the dry run remained reversible.
    JupyterLite notebook output for mqtt-sqlite-time-series 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)
  8. 8 Step 8

    Do
    Run `python3 main.py --step 8` to copy the database with SQLite's backup API and query the restored copy.
    You will see
    The source and restored SELECT statements each return 6, followed by `Comparison: row counts match, so the restored sample is readable.`
    Why it matters
    Running the same read against both databases demonstrates recoverability instead of treating a completed backup call as sufficient evidence.
    JupyterLite notebook output for mqtt-sqlite-time-series step 8, captured after running the real Python cell.
    Step 8 · 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. Which retention release criterion is strongest before deleting raw telemetry?

    Return to the chapter’s knowledge check
  2. Why does a time-partitioned store use chunk- or partition-drop deletion instead of a large DELETE WHERE time < cutoff job?

    Return to the chapter’s knowledge check
  3. A team wants to shorten raw telemetry retention because storage is growing. What should they do before changing the deletion policy?

    Return to the chapter’s knowledge check

Caution

The public broker is shared and unauthenticated; publish only the fictional fixture. This in-memory SQLite exercise demonstrates evidence and transactions, not production concurrency, partition dropping, legal-hold enforcement, durable backup storage, or access control.

Return to Data Retention and Downsampling · Browse Labs