IoT and AI

Jetson Nano MQTT: Publish GPU Edge Vision Results Securely

Build an edge vision pipeline on an NVIDIA Jetson: run GPU-accelerated detection, publish compact JSON results and device health over MQTT with TLS, and run it as a service that survives reboots.

Jetson Nano MQTT illustration with an NVIDIA Jetson board, a CSI camera and detection results sent over TLS to a cloud MQTT 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

A Jetson Nano MQTT pipeline lets you run GPU-accelerated vision at the edge and send only the results to the cloud. In this tutorial you will run object detection on an NVIDIA Jetson, publish detections and device health as JSON over MQTT with TLS, and turn the script into a service that starts on boot and reconnects on its own.

Why Jetson Plus MQTT

NVIDIA Jetson boards pair an Arm CPU with a CUDA-capable GPU in a small, low-power module. That makes them a popular choice for cameras that must detect people, vehicles or defects in real time without sending video to a data center.

The output of such a camera is the interesting part: object labels, positions, counts and alerts. MQTT is a natural way to deliver that output because it is lightweight, handles unreliable links gracefully and lets any number of consumers subscribe. The Jetson publishes once, and dashboards, alerting tools, historians and other AI services all receive the same messages.

Choosing a Jetson and Software Stack

Board JetPack generation Default Python paho-mqtt 2.x support Notes
Jetson Nano (original, 4 GB) JetPack 4.x 3.6 No, use 1.6.x Still widely deployed, no newer JetPack
Jetson Orin Nano JetPack 5 or 6 3.8 or 3.10 Yes Current entry-level choice
Jetson Orin NX / AGX Orin JetPack 5 or 6 3.8 or 3.10 Yes More GPU for larger models

This version difference is the most common pitfall. paho-mqtt 2.x requires Python 3.7 or newer, so on an original Jetson Nano running JetPack 4.6 you either install paho-mqtt 1.6.1 (and drop the CallbackAPIVersion argument and the properties callback parameter) or run your code in a container with a newer Python. The examples below target JetPack 5 or 6 with paho-mqtt 2.x.

For inference, this tutorial uses NVIDIA's open-source jetson-inference library, which wraps TensorRT-optimized detection models behind a short Python API and handles CSI and USB cameras. The MQTT part works the same with any other inference framework, including YOLO via Ultralytics.

Step 1: Prepare the Jetson

Install jetson-inference following its documentation (building from source or using its Docker container), then add the MQTT client:

sudo apt update
sudo apt install -y python3-pip mosquitto-clients
pip3 install "paho-mqtt>=2.0"

Confirm the camera works. With jetson-inference installed:

video-viewer csi://0          # CSI camera such as a Raspberry Pi Camera Module v2
video-viewer /dev/video0      # USB webcam

Then check that the Jetson can reach the broker. For a first test, use the public area of broker.connectmq.com over TLS:

mosquitto_pub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
  -i jetson-test-7d1e -t "connectmq/demo/jet-7d1e/hello" -m "jetson online"

If this fails, check outbound firewall rules for port 8883 and that the system clock is correct; TLS fails on a Jetson whose clock is years behind.

Step 2: The Detection Publisher

Save as jetson_mqtt.py. It captures frames, runs detectNet on the GPU, publishes throttled detections, retained per-class counts, and a heartbeat with frame rate and GPU-side performance.

import json
import ssl
import time
import uuid

import paho.mqtt.client as mqtt
from jetson_inference import detectNet
from jetson_utils import videoSource

BROKER, PORT = "broker.connectmq.com", 8883
PREFIX = "connectmq/demo/jet-7d1e/site1/gate/cam1"
USERNAME, PASSWORD = None, None  # set to "YOUR_USERNAME", "YOUR_PASSWORD" for a private broker
CLIENT_ID = f"jetson-cam1-{uuid.uuid4().hex[:6]}"  # use YOUR_CLIENT_ID on a private broker
PUBLISH_EVERY = 0.5
HEARTBEAT_EVERY = 30

net = detectNet("ssd-mobilenet-v2", threshold=0.5)
camera = videoSource("csi://0")  # or "/dev/video0", or "rtsp://..."


def on_connect(client, userdata, flags, reason_code, properties):
    print("connected:", reason_code)
    client.publish(f"{PREFIX}/status",
                   json.dumps({"online": True, "model": "ssd-mobilenet-v2"}),
                   qos=1, retain=True)


def on_disconnect(client, userdata, flags, reason_code, properties):
    print("disconnected:", reason_code)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID,
                     protocol=mqtt.MQTTv5)
if USERNAME:
    client.username_pw_set(USERNAME, PASSWORD)
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
client.will_set(f"{PREFIX}/status", json.dumps({"online": False}), qos=1, retain=True)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.connect_async(BROKER, PORT, keepalive=30)
client.loop_start()

last_pub = last_beat = 0.0
last_counts = None
frames = 0

while True:
    img = camera.Capture()
    if img is None:  # capture timeout
        continue
    detections = net.Detect(img, overlay="none")
    frames += 1

    objects, counts = [], {}
    for d in detections:
        label = net.GetClassDesc(d.ClassID)
        counts[label] = counts.get(label, 0) + 1
        objects.append({
            "label": label,
            "conf": round(d.Confidence, 2),
            "box": [round(d.Left / img.width, 3), round(d.Top / img.height, 3),
                    round(d.Right / img.width, 3), round(d.Bottom / img.height, 3)],
        })

    now = time.time()
    if now - last_pub >= PUBLISH_EVERY:
        last_pub = now
        client.publish(f"{PREFIX}/detections",
                       json.dumps({"ts": now, "objects": objects}), qos=0)

    if counts != last_counts:
        last_counts = counts
        client.publish(f"{PREFIX}/counts", json.dumps({"ts": now, **counts}),
                       qos=1, retain=True)

    if now - last_beat >= HEARTBEAT_EVERY:
        fps = frames / (now - last_beat) if last_beat else 0.0
        client.publish(f"{PREFIX}/health", json.dumps({
            "ts": now, "fps": round(fps, 1),
            "networkFps": round(net.GetNetworkFPS(), 1)}), qos=0)
        last_beat, frames = now, 0

Watch the results from your laptop:

mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
  -i jet-watch-7d1e -t "connectmq/demo/jet-7d1e/#" -v

connect_async plus loop_start means the script keeps running inference even when the network is down; paho reconnects in the background, and the retained status topic plus Last Will tell everyone whether the camera is reachable.

Step 3: Add Device Health

A camera that silently overheats or throttles is a common field failure. Jetson boards report temperatures through sysfs, which you can publish with the heartbeat:

import glob


def read_temps():
    temps = {}
    for zone in glob.glob("/sys/class/thermal/thermal_zone*"):
        try:
            with open(f"{zone}/type") as t, open(f"{zone}/temp") as v:
                temps[t.read().strip()] = int(v.read()) / 1000
        except OSError:
            pass
    return temps

Add "temps": read_temps() to the health payload. On a private broker, a threshold alert rule on a temperature field gives you a warning before the board starts throttling.

Step 4: Run as a Service

Create /etc/systemd/system/jetson-mqtt.service:

[Unit]
Description=Jetson vision to MQTT
After=network-online.target
Wants=network-online.target

[Service]
User=jetson
WorkingDirectory=/home/jetson/vision
ExecStart=/usr/bin/python3 /home/jetson/vision/jetson_mqtt.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now jetson-mqtt
journalctl -u jetson-mqtt -f

Designing Topics for Many Cameras

One Jetson is easy; twenty across three sites need a plan. Put the location hierarchy into the topic so that consumers can subscribe at the right level: site/area/line-or-gate/camera/.... A security dashboard subscribes to site1/+/+/alerts, an operations report subscribes to +/+/+/counts, and an engineer debugging one camera subscribes to that camera only. This is the same idea as an ISA-95 based Unified Namespace, and ConnectMQ recognizes such topic structures in its devices view.

Keep message types separate as shown in the script: high-rate detections, change-driven retained counts, low-rate health and a retained status with the Last Will. Mixing them in one topic forces every consumer to parse and filter everything, and makes it impossible to give different message types different QoS levels.

Performance Tips

Most of the frame rate on a Jetson comes from the inference engine, not from Python or MQTT. A few habits keep the pipeline fast:

  • Use TensorRT-optimized models; jetson-inference builds and caches the engine on first run, so the first start is slow and later starts are quick.
  • Avoid drawing overlays (overlay="none") when nobody watches the video.
  • Throttle publishing as in the script; JSON encoding for every frame at 30 fps adds CPU load for no benefit.
  • Use sudo jetson_clocks only when you have adequate cooling, and monitor temperatures through the health topic.

Jetson Nano MQTT Troubleshooting Checklist

  1. TypeError about CallbackAPIVersion. You are on paho-mqtt 1.x; upgrade, or on the original Nano use the 1.6 API.
  2. TLS handshake errors. Check the system date (timedatectl) and that ca-certificates is installed.
  3. Repeated disconnects with the same reason. Two devices share a client ID; every client must have a unique ID, and on ConnectMQ the client ID is bound to the issued credentials.
  4. Low frame rate after a while. Check the published temperatures; add a fan or select a lower power mode deliberately with nvpmodel.
  5. Message flood. Throttle detections; on the public area the limit is 10 messages per second per client.
  6. Camera not found under systemd. The service user needs to be in the video group.

From Prototype to Fleet

Once one camera works, the questions become operational: credentials per device, encryption, and seeing which cameras are online. Move from the public MQTT test broker to a private hosted MQTT broker as soon as real images or locations are involved. ConnectMQ gives each Jetson its own generated credentials, accepts TLS on 8883, lists devices by ISA-95 level and shows every detection in the live topic explorer. When many Jetsons in different sites report into one place, a cloud MQTT broker avoids opening inbound ports at any site.

For a model-agnostic version of this pipeline, see YOLO object detection over MQTT; for a microcontroller-sized alternative, see ESP32-CAM person detection; and for choosing between Jetson, Coral and other accelerators, read edge AI hardware. The AI and IoT guide covers the bigger picture.

Connect Your Jetson to ConnectMQ

Create a broker on the free Serverless plan at app.connectmq.com, no credit card required, then set BROKER, USERNAME, PASSWORD and CLIENT_ID in the script to the generated values. Your Jetson's detections will appear in the portal within seconds.

Frequently Asked Questions

Does paho-mqtt work on the original Jetson Nano?

Yes, but JetPack 4.x ships Python 3.6, so use paho-mqtt 1.6.1 and its older callback API, or run a container with Python 3.7 or newer for paho-mqtt 2.x.

Should the Jetson publish video frames over MQTT?

Generally no. Publish detections, counts and occasional snapshots within the message size limit, and use a streaming protocol if you need live video.

Which port should a Jetson use for MQTT?

Use 8883 with TLS for anything beyond a quick test. Port 1883 is unencrypted and should be limited to development with synthetic data.

Can I run MQTT and inference in the same Python process?

Yes. The paho network loop runs in a background thread via loop_start, so inference in the main loop is not blocked by network operations.

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.