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.
Sending Downlinks
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
- Run the bridge as a systemd service or container with automatic restart.
- Keep the TTN API key in an environment variable or secret store, never in the script.
- Give the API key only the rights it needs (reading application traffic and, if used, writing downlinks).
- Monitor
bridge/status: the last will turns itofflineif the process dies. - 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.
Why is decoded_payload missing from my uplinks?
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.



