IoT and AI

Ollama MQTT Bridge: Run a Local LLM on Your Gateway and Publish AI Insights

A practical tutorial for running Ollama on an IoT gateway, subscribing to MQTT telemetry and publishing structured, AI-generated insights back to the broker without sending data to an external AI service.

Ollama MQTT bridge on an edge gateway reading sensor topics and publishing AI insights to a broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Combining Ollama and MQTT lets you run a language model next to your machines and feed it live sensor data, without sending raw readings to an external AI service. In this tutorial you will install Ollama on a Linux gateway, build a Python bridge that subscribes to telemetry topics, asks a local model for structured insights and publishes the results back to the broker for dashboards and alerting.

Why Run the LLM on the Gateway?

A local model is not always the right choice, but on an edge gateway it solves real problems:

  • Data stays on site. Readings are processed locally; only the short insight leaves the gateway, if you choose to publish it upstream.
  • No dependency on internet AI services. The analysis keeps working through outages and does not depend on external API quotas.
  • Predictable cost. Once the hardware is in place there is no per-request charge.

The trade-offs are smaller models, slower generation on modest hardware and the work of keeping models updated. For explanations, classifications and short summaries of numeric data, small models are usually adequate. Our edge AI vs cloud AI comparison covers this decision in more depth.

Gateway class What to expect
Raspberry Pi 5 with 8 GB RAM Small models (1B to 3B parameters) run on CPU; responses take seconds, so analyze events, not every message
Mini PC with recent x86 CPU and 16 GB RAM Comfortable with 3B to 8B models for periodic analysis
Jetson or a PC with a GPU Larger models and faster responses, more frequent analysis

Architecture

sensors --MQTT--> broker --> ollama_bridge.py --HTTP localhost:11434--> Ollama
                    ^                |
                    |                v
                    +---- .../insights (JSON) <--+

The bridge is a normal MQTT client. It keeps the network loop running in a background thread, puts interesting messages on a queue and processes them one at a time with the model. That separation is the most important design decision in this tutorial: an LLM call can take several seconds, and blocking the MQTT callback that long causes missed keepalives and disconnects.

Step 1: Install Ollama and Pull a Model

On a 64-bit Linux gateway:

curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3.2:3b
ollama run llama3.2:3b "Reply with the word ready."

The installer registers a systemd service, so the Ollama API listens on http://localhost:11434 after reboot. Keep it bound to localhost; the bridge runs on the same machine and there is no reason to expose the model API to the network.

Install the Python dependencies in a virtual environment:

python3 -m venv ~/ollama-mqtt/.venv
source ~/ollama-mqtt/.venv/bin/activate
pip install ollama "paho-mqtt>=2.0"

Step 2: Define What an Insight Looks Like

Free text is hard to use downstream. Ollama supports structured outputs: you pass a JSON schema in the format parameter and the model's reply is constrained to that shape. Dashboards and alert rules can then read fields like severity directly.

{
  "type": "object",
  "properties": {
    "severity": { "type": "string", "enum": ["normal", "watch", "warning", "critical"] },
    "summary": { "type": "string" },
    "likely_causes": { "type": "array", "items": { "type": "string" }, "maxItems": 3 },
    "recommended_check": { "type": "string" }
  },
  "required": ["severity", "summary", "recommended_check"]
}

Step 3: Build the Ollama MQTT Bridge

The bridge subscribes to telemetry, keeps a short history per device and triggers an analysis when a reading crosses a simple rule or when the device's analysis interval has elapsed. Replace 4c7a2e with your own random string.

# ollama_bridge.py
import json
import logging
import queue
import threading
import time
from collections import defaultdict, deque

import ollama
import paho.mqtt.client as mqtt

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("bridge")

BROKER, PORT = "broker.connectmq.com", 1883
PREFIX = "connectmq/demo/4c7a2e/site1"
MODEL = "llama3.2:3b"
HISTORY = 30                 # readings kept per device
MIN_INTERVAL = 120           # seconds between analyses per device
RULES = {"temp_c": 70.0, "vib_mm_s": 5.0}

SCHEMA = {
    "type": "object",
    "properties": {
        "severity": {"type": "string", "enum": ["normal", "watch", "warning", "critical"]},
        "summary": {"type": "string"},
        "likely_causes": {"type": "array", "items": {"type": "string"}, "maxItems": 3},
        "recommended_check": {"type": "string"},
    },
    "required": ["severity", "summary", "recommended_check"],
}

SYSTEM = (
    "You analyze industrial sensor readings for maintenance technicians. "
    "Base your answer only on the readings provided. "
    "Phrase causes as possibilities, never as certainties. Keep the summary under 40 words."
)

history = defaultdict(lambda: deque(maxlen=HISTORY))
last_analysis = defaultdict(float)
work = queue.Queue(maxsize=50)


def on_connect(client, userdata, flags, reason_code, properties):
    log.info("connected: %s", reason_code)
    client.subscribe(f"{PREFIX}/+/telemetry", qos=1)


def on_message(client, userdata, msg):
    device = msg.topic.split("/")[-2]
    try:
        reading = json.loads(msg.payload)
    except json.JSONDecodeError:
        return
    history[device].append(reading)
    breached = any(isinstance(reading.get(k), (int, float)) and reading[k] > v for k, v in RULES.items())
    due = time.time() - last_analysis[device] > MIN_INTERVAL
    if (breached or due) and len(history[device]) >= 5:
        last_analysis[device] = time.time()
        try:
            work.put_nowait((device, list(history[device]), breached))
        except queue.Full:
            log.warning("queue full, skipping %s", device)


def analyze(device, readings, breached):
    prompt = (
        f"Device: {device}\nRule thresholds: {json.dumps(RULES)}\n"
        f"Triggered by threshold breach: {breached}\n"
        f"Last {len(readings)} readings, oldest first:\n{json.dumps(readings)}"
    )
    resp = ollama.chat(
        model=MODEL,
        messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}],
        format=SCHEMA,
        options={"temperature": 0},
    )
    return json.loads(resp.message.content)


def worker(client):
    while True:
        device, readings, breached = work.get()
        started = time.time()
        try:
            insight = analyze(device, readings, breached)
        except Exception as exc:
            log.error("analysis failed for %s: %s", device, exc)
            continue
        insight.update({
            "device": device,
            "model": MODEL,
            "trigger": "threshold" if breached else "interval",
            "durationS": round(time.time() - started, 1),
            "ts": int(time.time() * 1000),
        })
        client.publish(f"{PREFIX}/{device}/insights", json.dumps(insight), qos=1, retain=True)
        log.info("%s -> %s: %s", device, insight["severity"], insight["summary"])


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="ollama-bridge-4c7a2e")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT, keepalive=60)
threading.Thread(target=worker, args=(client,), daemon=True).start()
client.loop_forever()

How it behaves:

  1. Every telemetry message is stored in a bounded per-device history.
  2. A threshold breach or an elapsed interval queues an analysis job, but never more than one per device per interval.
  3. A single worker thread calls Ollama, so the gateway CPU is not overloaded by parallel generations.
  4. The insight is published retained, so dashboards show the latest assessment as soon as they subscribe.

Choosing a Model for the Gateway

Model choice on a gateway is a balance between quality, speed and memory. A few practical guidelines:

  1. Start small. A 3B model is a sensible default for structured insights from short readings. Move up only if the answers are clearly too shallow for your use case.
  2. Prefer quantized builds. The default tags in the Ollama library are already quantized, which is what makes small hardware viable. Avoid full-precision variants on CPU-only gateways.
  3. Leave memory for everything else. The gateway still runs the MQTT bridge, possibly a local broker, protocol converters and the operating system. Watch memory with free -h while a generation is running.
  4. Test with your own data. Collect a few dozen real telemetry windows, including known incidents, and compare how candidate models describe them. Consistency matters more than eloquence for operations.
  5. Pin the model version. Record the model tag in every published insight, as the bridge does, so you can tell which model produced a given recommendation after an upgrade.

Step 4: Feed It Test Data

Publish a few normal readings and then a hot one using the Mosquitto CLI:

for i in 1 2 3 4 5; do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-4c7a2e -q 1 \
    -t "connectmq/demo/4c7a2e/site1/pump-2/telemetry" \
    -m "{\"temp_c\": 6$i.0, \"vib_mm_s\": 2.$i, \"ts\": $(date +%s)000}"
  sleep 1
done
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-4c7a2e -q 1 \
  -t "connectmq/demo/4c7a2e/site1/pump-2/telemetry" \
  -m "{\"temp_c\": 74.5, \"vib_mm_s\": 5.8, \"ts\": $(date +%s)000}"

Watch the insights arrive:

mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-4c7a2e -t "connectmq/demo/4c7a2e/site1/+/insights" -v

You should see a JSON insight with a severity such as warning, a short summary mentioning rising temperature and vibration, and a recommended check. The exact wording varies by model.

Tuning the Ollama MQTT Setup

Problem Cause Fix
Bridge disconnects during analysis LLM call running inside the MQTT callback Keep the queue and worker thread pattern shown above
First response very slow Model loading into memory Set a longer keep-alive for the model, for example OLLAMA_KEEP_ALIVE=24h in the service environment
Queue fills up Too many triggers for the hardware Raise MIN_INTERVAL, tighten rules or use a smaller model
Output fields missing Older Ollama without structured outputs Update Ollama and the Python library
Insights contradict the data Too much raw data or vague prompt Send computed statistics instead of raw lists, lower temperature

That last row matters as data volume grows. Rather than raw readings, pass min, max, mean and trend computed in Python; our tutorial on LLM sensor data summaries shows that facts-first approach in detail.

Going to Production

On the gateway, run the bridge as a systemd service next to Ollama, and connect it to a private broker over TLS:

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="YOUR_CLIENT_ID")
client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.tls_set()
client.connect("YOUR_BROKER_HOST", 8883, keepalive=60)

The broker can be a local one on the gateway, a managed MQTT broker such as ConnectMQ, or both, with the gateway publishing only insights upstream. A cloud endpoint is useful when several sites need to share insights with central dashboards: the ConnectMQ portal shows them in the live topic explorer as they arrive. The public MQTT test broker is perfect for this tutorial, but its topics are readable by everyone, so keep real plant data off it.

Next steps: let operators ask questions about the same data with an MCP server for MQTT, or allow the model to act on devices safely with the patterns in natural language device control. For an overview of where local models fit among other AI techniques, see our AI and IoT guide.

Get Started with ConnectMQ

Share your gateway's AI insights across sites with ConnectMQ, a hosted broker with MQTT 5, TLS and WebSockets for browser dashboards. Create a free Serverless broker, no credit card required, and publish your first local LLM insight in minutes.

Frequently Asked Questions

Can a Raspberry Pi run Ollama?

Yes, a Raspberry Pi 5 with 8 GB of RAM can run small models on its CPU. Responses take seconds, so analyze events and periodic windows rather than every message.

Does Ollama speak MQTT directly?

No. Ollama exposes an HTTP API on localhost. A small bridge program connects to the broker as an MQTT client and calls the Ollama API, as shown in this tutorial.

How do I get reliable JSON from the model?

Pass a JSON schema in Ollama's format parameter to use structured outputs, set temperature to 0 and still validate the result before publishing.

Should every MQTT message trigger an LLM call?

No. Trigger analysis on threshold breaches or at intervals, and limit calls per device. Language models are far slower than message rates on a busy topic.

Related articles

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.