Tutoriales de IoT

MQTT a SQLite con Python: guarda y consulta mensajes de sensores

Crea un suscriptor en Python que guarda cada mensaje MQTT en SQLite con marca de tiempo, topic, QoS y flag retain, usando escrituras por lotes y modo WAL, y consulta el historial, medias por hora y últimos valores con SQL estándar.

Script de Python que mueve datos de MQTT a SQLite con una tabla de mensajes de sensores con marca de tiempo
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Enviar datos de MQTT a SQLite con Python es la forma más sencilla de dar historial a tu proyecto IoT: un solo archivo, sin servidor de base de datos y con SQL completo para las consultas. En este tutorial construirás un suscriptor paho-mqtt que guarda cada mensaje del servidor MQTT público gratuito broker.connectmq.com con su marca de tiempo, topic y flags, escribe en lotes eficientes y sobrevive a las reconexiones. Después consultarás los datos para obtener valores recientes, medias por hora y limpiar los registros antiguos.

¿Por qué SQLite para datos MQTT?

Un broker MQTT entrega los mensajes a quien esté suscrito en ese momento. No es una base de datos y, aparte de un retained message por topic, no guarda historial. Si quieres responder a "¿qué temperatura había a las 3 de la madrugada?", algo tiene que suscribirse y almacenar.

SQLite es una excelente primera opción para ese "algo":

Necesidad Cómo la resuelve SQLite
Cero configuración Incluido en la biblioteca estándar de Python (sqlite3)
Un solo archivo Fácil de respaldar, copiar o abrir en un navegador gráfico
Consultas SQL Filtrado, agrupación, funciones JSON y funciones de fecha
Tasas de escritura moderadas Miles de inserciones por segundo si se agrupan en transacciones
Dispositivos edge Funciona sin problemas en una Raspberry Pi o un PC industrial

No es la herramienta adecuada para muchos escritores concurrentes en máquinas distintas ni para años de datos de alta frecuencia de toda una flota. Para esos casos, consulta la guía de MQTT a TimescaleDB o cómo guardar datos MQTT en InfluxDB. Para un único emplazamiento, un gateway o un prototipo, SQLite es difícil de superar.

Arquitectura: mantén libre el hilo de red

El enfoque ingenuo ejecuta un INSERT y un commit() dentro de on_message. Funciona con unos pocos mensajes por minuto, pero cada commit fuerza una sincronización con el disco. A tasas más altas el callback se queda atrás, el bucle de red de paho se bloquea y los pings de keepalive pueden retrasarse lo suficiente como para que el broker cierre la conexión.

El diseño robusto separa las dos tareas:

  1. paho ejecuta su bucle de red en un hilo en segundo plano (loop_start()).
  2. on_message se limita a poner el mensaje en una queue.Queue.
  3. El hilo principal es dueño de la conexión SQLite, saca los mensajes de la cola y los escribe por lotes, haciendo commit una vez por lote o una vez por segundo.

Esto también evita la regla de SQLite según la cual una conexión debe usarse desde el hilo que la creó.

Paso 1: instalación y preparación

Solo necesitas paho-mqtt; sqlite3 viene con Python:

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install "paho-mqtt>=2.0"

Elige un prefijo de topic aleatorio para que tus datos no se mezclen con los de otros usuarios del servidor público. Este tutorial usa connectmq/demo/k7f3q2.

Paso 2: el logger de MQTT a SQLite en Python

Guarda esto como mqtt_sqlite_logger.py:

import queue
import signal
import sqlite3
import time
import uuid
from datetime import datetime, timezone

import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PORT = 1883
TOPIC = "connectmq/demo/k7f3q2/#"
DB_PATH = "mqtt_messages.db"
BATCH_SIZE = 200
FLUSH_INTERVAL = 1.0  # segundos

SCHEMA = """
CREATE TABLE IF NOT EXISTS messages (
    id          INTEGER PRIMARY KEY,
    received_at TEXT    NOT NULL,
    topic       TEXT    NOT NULL,
    payload     TEXT,
    qos         INTEGER NOT NULL,
    retained    INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_topic_time ON messages (topic, received_at);
CREATE INDEX IF NOT EXISTS idx_messages_time ON messages (received_at);
"""

inbox: "queue.Queue[tuple]" = queue.Queue(maxsize=50_000)
running = True


def utc_now() -> str:
    # 2026-10-16T08:15:30.123Z, ordenable y compatible con las funciones de fecha de SQLite
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"Connect failed: {reason_code}")
        return
    print(f"Connected, subscribing to {TOPIC}")
    client.subscribe(TOPIC, qos=1)


def on_message(client, userdata, msg):
    row = (
        utc_now(),
        msg.topic,
        msg.payload.decode("utf-8", errors="replace"),
        msg.qos,
        int(msg.retain),
    )
    try:
        inbox.put_nowait(row)
    except queue.Full:
        print("Queue full, dropping message")  # la base de datos no da abasto


def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"Disconnected: {reason_code}, paho will reconnect")


def open_db(path: str) -> sqlite3.Connection:
    db = sqlite3.connect(path)
    db.execute("PRAGMA journal_mode=WAL")    # los lectores no bloquean al escritor
    db.execute("PRAGMA synchronous=NORMAL")  # seguro con WAL y mucho más rápido
    db.executescript(SCHEMA)
    return db


def flush(db: sqlite3.Connection, batch: list) -> None:
    if not batch:
        return
    with db:  # una transacción por lote
        db.executemany(
            "INSERT INTO messages (received_at, topic, payload, qos, retained) VALUES (?, ?, ?, ?, ?)",
            batch,
        )
    print(f"Stored {len(batch)} messages")
    batch.clear()


def stop(*_):
    global running
    running = False


def main():
    signal.signal(signal.SIGINT, stop)
    signal.signal(signal.SIGTERM, stop)

    db = open_db(DB_PATH)

    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"sqlite-{uuid.uuid4().hex[:8]}")
    # Broker privado de ConnectMQ:
    # client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
    # client.tls_set(); y usa el puerto 8883
    client.on_connect = on_connect
    client.on_message = on_message
    client.on_disconnect = on_disconnect
    client.reconnect_delay_set(min_delay=1, max_delay=60)
    client.connect_async(BROKER, PORT, keepalive=60)
    client.loop_start()

    batch: list = []
    last_flush = time.monotonic()
    try:
        while running:
            try:
                batch.append(inbox.get(timeout=0.2))
            except queue.Empty:
                pass
            if len(batch) >= BATCH_SIZE or time.monotonic() - last_flush >= FLUSH_INTERVAL:
                flush(db, batch)
                last_flush = time.monotonic()
    finally:
        client.disconnect()
        client.loop_stop()
        while not inbox.empty():
            batch.append(inbox.get_nowait())
        flush(db, batch)
        db.close()
        print("Logger stopped cleanly")


if __name__ == "__main__":
    main()

Ejecútalo con python mqtt_sqlite_logger.py. En otra terminal, publica algunos datos de prueba:

for i in $(seq 1 20); do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i sqlite-test-pub \
    -t "connectmq/demo/k7f3q2/sensors/room$((i % 2 + 1))" \
    -m "{\"temperature\": $((20 + RANDOM % 5)).$((RANDOM % 10)), \"humidity\": $((40 + RANDOM % 10))}"
  sleep 1
done

El logger imprime "Stored 1 messages" más o menos una vez por segundo. Detenlo con Ctrl+C y la cola pendiente se vuelca antes de salir.

Cómo se mantiene fiable el logger

Marcas de tiempo. El logger registra cuándo se recibió el mensaje, en UTC y con milisegundos. Los relojes de los dispositivos derivan y algunos ni siquiera tienen reloj, así que la marca de recepción es una referencia fiable. Si tu payload incluye su propia marca de tiempo, guarda ambas. La guía de JSON con MQTT en Python muestra cómo añadirla en el lado del publicador.

Modo WAL. El write-ahead logging permite que un dashboard o la CLI sqlite3 lean la base de datos mientras el logger escribe. Sin él, las lecturas largas pueden bloquear las inserciones.

Cola acotada. Si el disco se atasca, la cola crece hasta 50.000 mensajes y después descarta los nuevos mostrando un aviso, en lugar de agotar la memoria. Ajusta el tamaño a tu tráfico.

Reconexiones. connect_async() junto con loop_start() sigue reintentando en segundo plano, y la suscripción se restaura en on_connect. Los mensajes publicados mientras el logger está desconectado se pierden salvo que uses una sesión persistente. Con MQTT 5 puedes fijar un session expiry interval y clean_start=False para que el broker encole los mensajes QoS 1 por ti; el broker público conserva las sesiones hasta 30 minutos. La guía de reconexión de paho-mqtt explica los detalles.

Retained messages. En cada reconexión el broker vuelve a enviar los retained messages, así que los verás guardados más de una vez. La columna retained te permite excluirlos del análisis.

Paso 3: consulta tu historial MQTT

Abre la base de datos con la CLI de SQLite (sqlite3 mqtt_messages.db) o con cualquier navegador SQLite y prueba estas consultas.

Los últimos 10 mensajes de un topic:

SELECT received_at, payload
FROM messages
WHERE topic = 'connectmq/demo/k7f3q2/sensors/room1'
ORDER BY received_at DESC
LIMIT 10;

Último valor por topic, usando el comportamiento documentado de SQLite para columnas sueltas con MAX():

SELECT topic, payload, MAX(received_at) AS last_seen
FROM messages
WHERE retained = 0
GROUP BY topic;

Medias por hora a partir de payloads JSON, con las funciones JSON integradas de SQLite:

SELECT strftime('%Y-%m-%d %H:00', received_at)            AS hour,
       topic,
       ROUND(AVG(json_extract(payload, '$.temperature')), 2) AS avg_temperature,
       COUNT(*)                                             AS samples
FROM messages
WHERE topic LIKE 'connectmq/demo/k7f3q2/sensors/%'
  AND json_valid(payload)
GROUP BY hour, topic
ORDER BY hour;

Conservar solo los últimos siete días y recuperar espacio:

DELETE FROM messages
WHERE received_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days');
VACUUM;

Como received_at se guarda en un formato ISO 8601 fijo, la simple comparación de cadenas ordena correctamente y el filtro por rango de tiempo puede usar el índice. Puedes lanzar la consulta de retención desde cron o desde el propio logger una vez al día.

Para trabajar con los datos en Python, pandas.read_sql_query("SELECT ...", sqlite3.connect("mqtt_messages.db")) devuelve un DataFrame. El tutorial de MQTT y pandas cubre el análisis y el remuestreo.

Lista de comprobación para resolver problemas

  1. "database is locked": otro proceso mantiene una transacción de escritura. Asegúrate de que el modo WAL está activado y de que solo un logger escribe en el archivo.
  2. No se guarda nada: confirma que el filtro de topic coincide con el topic del publicador; pruébalo con mosquitto_sub -h broker.connectmq.com -p 1883 -i check-k7f3q2 -t 'connectmq/demo/k7f3q2/#' -v.
  3. json_extract devuelve NULL: el nombre de la clave o la ruta JSON no coinciden, o el payload no es JSON. Revisa antes algunas filas en bruto.
  4. La base de datos crece muy rápido: añade la tarea de retención y plantéate guardar solo los valores que cambian en el lado del publicador.
  5. El logger descarta mensajes: aumenta BATCH_SIZE, mueve la base de datos a un almacenamiento más rápido o limita la suscripción a los topics que realmente necesitas.

Del prototipo a producción

En instalaciones reales, ejecuta el logger como servicio (systemd en Linux) para que se reinicie tras un reinicio del equipo, y apúntalo a un broker privado en lugar del público. Con ConnectMQ creas un broker en el portal, copias el usuario, la contraseña y el client ID generados y activas TLS en el puerto 8883; el tutorial de MQTT con TLS en Python muestra la configuración exacta. El broker de ConnectMQ no almacena por sí mismo el historial de mensajes, así que un suscriptor externo como este es el patrón honesto y recomendado. Si necesitas un punto de partida para el código del cliente, consulta el tutorial de paho mqtt en Python y, para otros lenguajes, el resumen de bibliotecas cliente MQTT. La documentación de sqlite3 de Python trata en profundidad las transacciones y las opciones de conexión.

Empieza con ConnectMQ

ConnectMQ es un broker MQTT gestionado con MQTT 3.1.1 y 5.0, TLS y WebSocket, además de un explorador de topics en vivo para comprobar qué envían tus dispositivos antes de registrarlo. Empieza hoy a registrar datos desde broker.connectmq.com y pasa después a tu propio broker MQTT en la nube. Crea una cuenta gratuita en el plan Serverless, sin tarjeta de crédito.

Preguntas frecuentes

¿Puede SQLite manejar datos MQTT continuos?

Sí, para un único logger a tasas moderadas. Agrupar las inserciones en transacciones y activar el modo WAL permite a SQLite absorber un flujo constante de muchos sensores en hardware modesto.

¿Por qué no insertar directamente dentro de on_message?

Hacer commit en el callback bloquea el bucle de red de paho con la E/S de disco. Bajo carga eso retrasa los pings de keepalive y los acuses de recibo, así que una cola con un escritor separado es más fiable.

¿Cómo evito perder mensajes mientras el logger se reinicia?

Usa QoS 1 con una sesión persistente de MQTT 5 (clean_start=False y un session expiry interval) para que el broker encole los mensajes mientras estás desconectado, dentro de su límite de expiración de sesión.

¿Puedo guardar payloads binarios en SQLite?

Sí. Cambia la columna payload a BLOB e inserta msg.payload directamente en lugar de decodificarlo a texto.

Artículos relacionados

Las mejores herramientas CLI de MQTT para pruebas, scripts y depuración

Una comparativa práctica de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI de MQTT.js, curl y mqttui, con la misma prueba ejecutada contra broker.connectmq.com para que elijas la herramienta adecuada para scripts y depuración.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.