IoT Automation

The Things Network MQTT Integration: Bridge LoRaWAN Uplinks to Your Broker

Subscribe to The Things Network's MQTT server, decode LoRaWAN uplinks with a payload formatter, and republish clean sensor JSON into your own topic tree with a short Python bridge.

The Things Network MQTT bridge carrying LoRaWAN sensor uplinks from a gateway into a cloud MQTT broker topic tree
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

The Things Network MQTT server gives every application a live feed of its LoRaWAN uplinks, which makes it easy to pull long-range sensor data into your own systems. In this tutorial you will write a payload formatter, connect to the TTN MQTT server with an API key, and run a small Python bridge that republishes decoded readings as clean JSON into your own broker topic tree, where dashboards, Node-RED and automations can use them alongside your other devices.

Why Bridge TTN into Your Own Broker

The Things Network (TTN) handles the radio side of LoRaWAN: gateways, device activation, deduplication and decryption. Its MQTT server publishes each uplink as a large JSON document containing metadata from every gateway that heard the packet. That is excellent for debugging but awkward as a long-term data model:

  • topics are TTN's (v3/<app>@ttn/devices/<device>/up), not your site hierarchy;
  • payloads are big and nested, while most consumers want three or four values;
  • your Wi-Fi devices, PLCs and controllers already live on another broker.

A bridge solves all three. It subscribes to TTN, extracts what matters and publishes compact messages into the same topic tree as everything else. Consumers never need TTN credentials, and if you later move to a private network server, only the bridge changes. The smart agriculture IoT guide explains where this fits in a farm-wide design.

Step 1: Decode the Payload in TTN

LoRaWAN devices send compact bytes. Add a payload formatter in the TTN Console under Applications > your app > Payload formatters > Uplink, type Custom Javascript formatter. This example decodes a soil node that sends moisture (uint16, tenths of a percent), soil temperature (int16, tenths of a degree) and battery (uint16, millivolts):

function decodeUplink(input) {
  var b = input.bytes;
  if (input.fPort !== 1 || b.length < 6) {
    return { errors: ["unexpected port or length"] };
  }
  var moisture = ((b[0] << 8) | b[1]) / 10;
  var tRaw = (b[2] << 8) | b[3];
  if (tRaw & 0x8000) tRaw -= 0x10000;
  var battery = ((b[4] << 8) | b[5]) / 1000;
  return {
    data: {
      moisture_pct: moisture,
      soil_temp_c: tRaw / 10,
      battery_v: battery
    },
    warnings: [],
    errors: []
  };
}

Use the Test panel with a hex payload such as 0136 008C 0E74 to confirm the output before any device is deployed. The decoded object appears in each uplink as uplink_message.decoded_payload.

Step 2: Get MQTT Credentials from TTN

In the Console, open Integrations > MQTT. You will see:

Setting Value
Server Your cluster, for example eu1.cloud.thethings.network
Ports 1883 (plain) and 8883 (TLS)
Username <application-id>@ttn
Password An API key; click Generate new API key

Copy the API key immediately; TTN shows it only once. Always use port 8883 to TTN, since the key grants access to your application data. The MQTT server section of the official The Things Stack documentation lists the full topic set, including join, downlink and service events.

The uplink topic for one application is:

v3/<application-id>@ttn/devices/<device-id>/up

You can check the feed before writing any code:

mosquitto_sub -h eu1.cloud.thethings.network -p 8883 --capath /etc/ssl/certs \
  -u 'my-farm-app@ttn' -P 'NNSXS.YOUR_TTN_API_KEY' \
  -t 'v3/my-farm-app@ttn/devices/+/up' -v

Step 3: The Things Network MQTT Bridge in Python

The bridge uses two paho-mqtt 2.x clients: one subscribed to TTN, one publishing to your broker. Run it first against the public test broker, replacing t6n1b4 with your own random string because the public area is readable by anyone.

import json
import os
import ssl

import paho.mqtt.client as mqtt

TTN_HOST = "eu1.cloud.thethings.network"
TTN_APP = os.environ.get("TTN_APP", "my-farm-app")
TTN_KEY = os.environ["TTN_API_KEY"]

OUT_HOST = os.environ.get("OUT_HOST", "broker.connectmq.com")
OUT_PORT = int(os.environ.get("OUT_PORT", "1883"))
OUT_USER = os.environ.get("OUT_USER")          # set in production
OUT_PASS = os.environ.get("OUT_PASS")
OUT_CLIENT_ID = os.environ.get("OUT_CLIENT_ID", "ttn-bridge-out-t6n1b4")
OUT_PREFIX = os.environ.get("OUT_PREFIX", "connectmq/demo/t6n1b4/farm")

# --- outgoing broker -------------------------------------------------------
out = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=OUT_CLIENT_ID)
if OUT_USER:
    out.username_pw_set(OUT_USER, OUT_PASS)
if OUT_PORT == 8883:
    out.tls_set(cert_reqs=ssl.CERT_REQUIRED)
out.will_set(f"{OUT_PREFIX}/bridge/status", "offline", qos=1, retain=True)

def out_on_connect(client, userdata, flags, reason_code, properties):
    print("out broker:", reason_code)
    client.publish(f"{OUT_PREFIX}/bridge/status", "online", qos=1, retain=True)

out.on_connect = out_on_connect
out.connect(OUT_HOST, OUT_PORT, keepalive=60)
out.loop_start()

# --- TTN ---------------------------------------------------------------------
def ttn_on_connect(client, userdata, flags, reason_code, properties):
    print("ttn:", reason_code)
    client.subscribe(f"v3/{TTN_APP}@ttn/devices/+/up", qos=1)

def ttn_on_message(client, userdata, msg):
    try:
        up = json.loads(msg.payload)
        device = up["end_device_ids"]["device_id"]
        um = up["uplink_message"]
        decoded = um.get("decoded_payload")
    except (ValueError, KeyError) as exc:
        print("skipping malformed uplink:", exc)
        return
    if not decoded:
        print(f"{device}: no decoded payload (check the formatter)")
        return

    rx = um.get("rx_metadata", [])
    best = max(rx, key=lambda g: g.get("rssi", -999)) if rx else {}
    record = {
        "ts": up.get("received_at"),
        **decoded,
        "rssi": best.get("rssi"),
        "snr": best.get("snr"),
        "gateways": len(rx),
        "f_cnt": um.get("f_cnt"),
    }
    topic = f"{OUT_PREFIX}/{device}/telemetry"
    out.publish(topic, json.dumps(record), qos=1, retain=True)
    print(topic, record)

ttn = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                  client_id=f"ttn-bridge-in-{TTN_APP}")
ttn.username_pw_set(f"{TTN_APP}@ttn", TTN_KEY)
ttn.tls_set(cert_reqs=ssl.CERT_REQUIRED)
ttn.on_connect = ttn_on_connect
ttn.on_message = ttn_on_message
ttn.connect(TTN_HOST, 8883, keepalive=60)
ttn.loop_forever()

Run it:

pip install paho-mqtt
export TTN_API_KEY='NNSXS.YOUR_TTN_API_KEY'
python ttn_bridge.py

And watch the clean output from another terminal:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-t6n1b4 \
  -t 'connectmq/demo/t6n1b4/farm/#' -v

Each uplink becomes one retained message such as {"ts":"2026-10-16T06:15:02Z","moisture_pct":31.0,"soil_temp_c":14.0,"battery_v":3.7,"rssi":-108,"snr":4.5,"gateways":2,"f_cnt":412}. Because it is retained, a dashboard that starts later still sees each sensor's last reading immediately, which matters for devices that report only every 15 minutes.

Design Notes

Why include RSSI, SNR and gateway count? They are the earliest warning of a failing antenna, a moved gateway or a sensor sinking into mud. Storing them costs almost nothing.

Why retained messages? LoRaWAN devices are quiet most of the time. Without retain, any consumer that connects between uplinks sees nothing until the next one.

Why QoS 1 on both sides? The bridge might restart while TTN delivers; QoS 1 with a reconnecting client reduces gaps. For the trade-offs, see MQTT QoS levels explained.

Topic depth. connectmq/demo/t6n1b4/farm/<device>/telemetry has six levels, comfortably inside the public test broker's eight-level limit. In production, use your own hierarchy such as farm1/north/<device>/telemetry.

Message size. A raw TTN uplink with metadata from several gateways can run to a few kilobytes. The republished record is a couple of hundred bytes, far below the 64 KB limit of the public test area and the 256 KB limit of the ConnectMQ free plan, and much cheaper for battery-backed dashboards and mobile clients to process.

Handling Gaps and Duplicates

LoRaWAN is a lossy radio link by design: devices transmit without acknowledgement, and a packet that no gateway hears is simply gone. The frame counter (f_cnt) included in each record lets consumers detect gaps. If the counter jumps from 410 to 413, two uplinks were lost on the radio side, not in MQTT. That distinction saves time when someone reports missing data.

The bridge can also see the same uplink twice if it reconnects to TTN while a QoS 1 message is in flight. Consumers that store data should treat the combination of device ID and frame counter as a unique key and ignore repeats. Note that the counter resets when a device rejoins the network, so use it for gap detection over short windows rather than as a permanent identifier.

To send a command to a device (for example a new reporting interval), the bridge can subscribe to a command topic on your broker and forward it to TTN's downlink topic:

import base64

def forward_downlink(device_id, port, raw_bytes):
    body = {"downlinks": [{
        "f_port": port,
        "frm_payload": base64.b64encode(raw_bytes).decode(),
        "priority": "NORMAL",
    }]}
    ttn.publish(f"v3/{TTN_APP}@ttn/devices/{device_id}/down/push",
                json.dumps(body), qos=1)

# example: set interval to 30 minutes (one byte) on port 2
# forward_downlink("soil-node-07", 2, bytes([30]))

Class A devices only receive downlinks after their next uplink, so commands arrive with a delay equal to the reporting interval. Keep downlinks rare; LoRaWAN airtime and duty-cycle rules make them expensive.

Running It Reliably

  1. Run the bridge as a systemd service or container with automatic restart.
  2. Keep the TTN API key in an environment variable or secret store, never in the script.
  3. Give the API key only the rights it needs (reading application traffic and, if used, writing downlinks).
  4. Monitor bridge/status: the last will turns it offline if the process dies.
  5. Remember TTN's fair-use policy for the community network; it is designed for low-frequency sensors, not chatty devices.

Production on a Private Broker

Set OUT_HOST=broker.connectmq.com, OUT_PORT=8883, OUT_USER, OUT_PASS and OUT_CLIENT_ID to the credentials issued with your private broker, and change OUT_PREFIX to your own hierarchy. The rest of the script stays the same. A hosted MQTT broker gives your LoRaWAN data, Wi-Fi sensors and controllers one shared, encrypted home that you do not have to maintain. If you run your own network server instead of TTN, the ChirpStack MQTT tutorial shows the equivalent bridge, and the soil moisture sensor MQTT build is a Wi-Fi alternative for fields near buildings. More patterns are collected in the IoT automation guide, and the background on what an MQTT broker does helps if MQTT is new to you.

Bring Your LoRaWAN Data to ConnectMQ

ConnectMQ gives your bridged TTN data a private MQTT broker with TLS, a live topic explorer, a dashboard and threshold alert rules for values like low battery or dry soil. Create a free Serverless broker at app.connectmq.com, no credit card required, and point the bridge's output at it.

Frequently Asked Questions

What is the TTN MQTT username and password?

The username is your application ID followed by @ttn (for The Things Network community edition), and the password is an API key generated under Integrations > MQTT.

No uplink payload formatter is configured, or it threw an error. Test it in the Console's formatter panel with a real hex payload.

Can I connect TTN straight to another broker without code?

TTN offers webhooks and other integrations, but to publish into your own MQTT broker in your own topic structure, a small bridge like this one is the simplest and most transparent option.

Does the bridge need to run near the gateway?

No. Both connections are over the internet, so it can run on any server, a Raspberry Pi or a container.

Related articles

Zigbee2MQTT Setup Guide: From Coordinator to Dashboard

Install Zigbee2MQTT with Docker, configure the coordinator and MQTT connection, pair and rename devices, control them over MQTT and view their states on a dashboard, testing first on a free public broker.

7 min read

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.