Skip to content

Embedded MQTT Broker & Bridge ​

Added in 4.6

The embedded MQTT broker and bidirectional bridges shipped in MeshMonitor 4.6 (PR #3053, follow-up client-proxy bridge in PR #3054, resolving issue #3003). Standalone bridges (no parent broker required) were added in a later release (issue #3134).

MeshMonitor can run its own MQTT broker inside the same container, so your Meshtastic devices (and any other MQTT clients on your network) can publish directly to MeshMonitor β€” and MeshMonitor can relay that traffic, with filtering rules, to public upstream brokers like mqtt.meshtastic.org.

What problem does this solve? ​

Public Meshtastic brokers aggregate nodes across an entire country or continent. When a device subscribes to a broad topic (msh/CA, msh/US) with downlink enabled, its nodeDB and the RF mesh get polluted with packets from nodes hundreds of kilometres away. Per-device workarounds (narrow topics, ignoreMqtt) are incomplete and require firmware-version-specific config on every node.

The embedded broker shifts this fight to the server, where MeshMonitor already has full nodeDB context. You point your devices at MeshMonitor's local broker, let MeshMonitor bridge to whatever public upstreams you want, and apply filters (topic patterns, channel/node/portnum allow-block lists, geographic bounding boxes) at the bridge instead of on the device. The local mesh stays clean regardless of firmware version.

Two source types ​

The feature adds two new source types in Dashboard β†’ Sources β†’ Add Source: an mqtt_broker that accepts MQTT connections (a server), and an mqtt_bridge that opens an MQTT connection (a client). They can run together, separately, or in any combination β€” picking the right one (or both) is the most common configuration question, so the rest of this section walks through each in detail and ends with a side-by-side feature matrix.

mqtt_broker β€” accept connections from devices and other MQTT clients ​

A self-contained MQTT broker, backed by Aedes (a pure-Node, MQTT 3.1.1 broker, MIT-licensed). MeshMonitor opens a TCP port (default 1883), advertises shared username/password authentication, and accepts MQTT 3.1.1 clients that connect to it.

What it does

  • Listens on 0.0.0.0:<port> (configurable; defaults to the IANA-registered MQTT port 1883).
  • Authenticates every CONNECT with a single shared username/password pair (default-deny: connections without configured credentials are rejected).
  • Decodes ServiceEnvelope packets from any client that publishes under its rootTopic (default msh), and ingests decodable payloads (NodeInfo, Position, TextMessage, Telemetry) into the database under this source's sourceId. Other clients subscribed to the broker still see the raw byte-for-byte publish, so devices can fan out to each other over MQTT just like they would on a public broker.
  • Optionally clamps hop_limit to 0 on every Meshtastic packet it delivers to a connected client ("Zero-hop injection" β€” see below), matching mqtt.meshtastic.org's behavior so MQTT-bridged packets don't trigger RF re-broadcasts.
  • Generates a synthetic gateway identity (nodeNum, !nodeId, longName, shortName) at create time so its publishes look like they're coming from a real node to upstream brokers.

When to use a broker

  • You want multiple devices to share a single set of MQTT credentials and converge on MeshMonitor instead of each holding its own upstream credentials.
  • You want other LAN clients (Home Assistant, Grafana, custom scripts) to subscribe to the same Meshtastic traffic without setting up a separate broker.
  • You need server-side ingestion of locally-published traffic β€” every NodeInfo / Position / Telemetry your devices publish via MQTT lands in the database under this source's sourceId, with no extra wiring.

mqtt_bridge β€” open a connection to an upstream broker ​

A long-lived MQTT client, backed by MQTT.js. MeshMonitor opens a connection out to one upstream MQTT server (mqtt.meshtastic.org, a regional community broker, your own private one), subscribes to a list of topics, and decodes whatever comes back.

What it does

  • Connects to one upstream URL (mqtt://… for plain TCP, mqtts://…:8883 for TLS) with optional username/password.
  • Subscribes to a configured list of upstream topics (MQTT wildcards: + single-segment, # multi-segment tail).
  • Decodes inbound ServiceEnvelope packets and ingests them into the database under this bridge's sourceId β€” so traffic the bridge receives from upstream shows up as its own MeshMonitor source.
  • Applies an independent downlink filter pipeline before ingestion and republish:
    • Topic patterns β€” allow / block lists
    • Channel name β€” allow / block lists
    • Node ID β€” allow / block lists (!xxxxxxxx)
    • PortNum β€” allow / block lists (Meshtastic PortNum enum)
    • Geographic bounding box β€” drop position packets whose latitude_i / 1e7, longitude_i / 1e7 falls outside { minLat, maxLat, minLng, maxLng }. Applied before republish so out-of-area positions never reach the local broker (when attached) or are injected into a device (when used as a client-proxy target).
  • Surfaces broker ACL state β€” if the upstream rejects authentication or denies your subscriptions, the bridge status shows permissionMessage so you know why traffic isn't flowing.
  • 60-second (topic, packetId) echo suppression on both directions prevents bridge feedback loops.

Two modes β€” with or without a parent broker

A bridge can run in either of two configurations, picked when you create it ("Parent broker (optional)" dropdown):

  1. Attached to a parent mqtt_broker (the default before the standalone option was added). In this mode the bridge is bidirectional:
    • Downlink (upstream β†’ local broker): ingests, applies the downlink filter pipeline, and republishes raw bytes onto the parent broker so locally-connected devices see the same wire format.
    • Uplink (local broker β†’ upstream): listens to the parent broker's local-packet event, applies an independent uplink filter pipeline (same filter shapes as downlink), and publishes raw bytes to the upstream broker.
  2. Standalone β€” no parent broker. The bridge runs as a pure MQTT client. Downlink still ingests; there's no local republish (no local broker to republish to) and no uplink event source. The bridge can still act as a client-proxy target for a meshtastic_tcp source's mqttLink: device traffic flows device β†’ MeshMonitor β†’ bridge.publish() β†’ upstream, with no embedded broker in between.

When to use a bridge

  • Attached: you have an embedded broker hosting one or more local devices and you want their traffic to fan out upstream β€” and/or you want upstream traffic from a curated topic set to reach those devices.
  • Standalone, monitoring: you want to watch what flows on an upstream broker (e.g. mqtt.meshtastic.org for a regional area) without hosting any local MQTT clients. The bridge subscribes, ingests, and persists; that data shows up as its own source in the sidebar.
  • Standalone, client-proxy target: you have a Meshtastic device in proxy_to_client mode and you want its MQTT traffic forwarded straight upstream without an intermediate embedded broker. The Meshtastic source's mqttLink points at the bridge directly. This is the MQTT-client-proxy use case (issue #3134).

Broker vs Bridge β€” feature matrix ​

Capabilitymqtt_brokermqtt_bridge (attached)mqtt_bridge (standalone)
RoleMQTT server β€” accepts connectionsMQTT client β€” opens one connectionMQTT client β€” opens one connection
Opens a TCP listener port?Yes (configurable, default 1883)NoNo
Connects to an upstream broker?No (it is the broker)Yes (one per bridge)Yes (one per bridge)
Locally-connected MQTT clients (devices, LAN apps)?Yes, manyVia the parent brokerNo
Multiple upstream brokers?n/aYes β€” N bridges, each independentYes β€” N bridges, each independent
Per-source filter pipeline (topic / channel / node / portnum / geo)?No (broker is dumb relay)Yes β€” downlink and uplinkYes β€” downlink only
Server-side ingestion of decoded packets?Yes β€” under broker sourceIdYes β€” under bridge sourceId (downlink)Yes β€” under bridge sourceId (downlink)
Echo suppression (no feedback loops)?n/aYes (60s topic+packetId cache)Yes
Zero-hop injection toggle?Yes (details)n/an/a
Synthetic gateway identity for outbound publishes?Yes (auto-generated at create)Inherited from parent brokern/a (no outbound until used as proxy target)
Default-deny authentication on the listener?Yesn/an/a
Can serve as a mqttLink client-proxy target?YesYesYes β€” primary use case
TLS / WSS?Plain TCP only in v1mqtts:// upstream supportedmqtts:// upstream supported
Survives a sibling source restart?Independent β€” broker keeps listening if a bridge restartsDetaches if parent broker stops; reattaches when it comes backIndependent β€” runs without any sibling
Status fields on /api/sources/:id/statuslistening, clientCount, packetsIn, packetsIngested, packetsDropped, lastErrorupstreamConnected, parentBrokerAttached, downlinkIn, downlinkIngested, downlinkRepublished, uplinkOut, downlink/uplink drop counters, permissionMessageSame as attached, but parentBrokerAttached: false and uplinkOut stays at 0
Required pair?Standalone β€” no bridge requiredRequires a sibling mqtt_brokerNone

Use-case recipes ​

  • "Filtered window onto the public Meshtastic broker." Create one standalone bridge to mqtt://mqtt.meshtastic.org with a topic pattern and geo bbox that matches your region. No broker needed. You get a sidebar source whose nodes / messages / positions are sourced from upstream, filtered before ingestion.
  • "My devices on the LAN should fan out to each other, plus selectively reach a public broker." Create one broker + one attached bridge. Devices connect to the broker (direct TCP or client-proxy). The bridge applies a filter pipeline in both directions and forwards the selected slice upstream.
  • "My BLE-only node should publish MQTT through MeshMonitor straight to a public broker, no embedded broker required." Create one standalone bridge pointed at the public broker. On the Meshtastic source, set mqttLink β†’ <bridge> (Sources β†’ Edit β†’ "Bridge MQTT proxy to", or use the Quick Configure dropdown on Device β†’ MQTT). Enable proxy_to_client_enabled on the firmware. Device β†’ MeshMonitor (over BLE/serial) β†’ bridge β†’ upstream.
  • "Same as above, plus I want a Home Assistant box on the LAN to subscribe to my devices." Create one broker + one attached bridge. Devices use client-proxy mode pointing at the broker; Home Assistant subscribes to the broker on port 1883. Bridge handles the upstream fan-out.
  • "Two upstream brokers, one regional and one global, with different filters per upstream." Create one broker + two attached bridges, each with its own topic/geo/portnum rules. Devices publish once; each bridge independently decides what to forward.

Three ways a device can reach MQTT through MeshMonitor ​

PathFirmware setupMeshMonitor setupWhen to use
Direct TCP to brokermqtt.enabled = true, address = <host>:1883, proxy_to_client_enabled = false, credentials matchCreate an mqtt_broker source; expose port 1883 in docker-compose.ymlDevices with reliable WiFi/Ethernet that can reach the MeshMonitor host on the LAN
Client-proxy β†’ brokermqtt.enabled = true, proxy_to_client_enabled = true, credentials/address still set on firmware (mostly decorative in proxy mode)Create an mqtt_broker source; set the Meshtastic source's mqttLink to the broker (Sources β†’ Edit β†’ "Bridge MQTT proxy to")Devices without WiFi (Serial/BLE-attached), behind NAT, or on networks where port 1883 isn't reachable β€” and you also want other LAN clients to subscribe to the embedded broker
Client-proxy β†’ standalone bridgeSame firmware setup as above (proxy_to_client_enabled = true)Create a standalone mqtt_bridge (no parent broker); set the Meshtastic source's mqttLink to the bridgeSame connectivity scenarios as client-proxy β†’ broker, but you don't need a local broker β€” device traffic goes straight upstream through the bridge (issue #3134)

In proxy mode, the firmware uses Meshtastic's MqttClientProxyMessage protocol: the device hands every outbound publish off as a FromRadio.mqttClientProxyMessage over its existing TCP/serial connection to MeshMonitor, and MeshMonitor publishes on its behalf. Inbound messages from the linked target (broker or bridge) are wrapped as ToRadio.MqttClientProxyMessage and injected back to the device. This is the same mechanism the Meshtastic mobile apps use when proxying.

The Device β†’ MQTT Quick Configure dropdown lists both brokers and bridges with type tags so you can pick either in one click; for bridges it parses the upstream URL into the firmware's MQTT address field so the device's local config reflects where its traffic is actually going.

Quick setup ​

1. Create the broker source ​

In Dashboard β†’ Sources β†’ Add Source, pick Embedded MQTT Broker (devices connect here) and fill in:

FieldNotes
NameAnything β€” shows in the sidebar
Listener portDefault 1883 (IANA-registered MQTT port)
Username / PasswordShared credential for all clients
Root topicDefault msh β€” must match what your devices publish under
Zero-hop injectionOff by default. When on, the broker clamps hop_limit to 0 on every Meshtastic packet it delivers to a connected device β€” matching the behavior of Meshtastic's public broker. See Zero-hop injection below for when to use it.

Save. MeshMonitor will start the broker; you'll see MQTT broker listening on 0.0.0.0:1883 in the container logs and a new source card in the sidebar.

If you plan to use the direct-TCP path, make sure your docker-compose.yml exposes port 1883:

yaml
services:
  meshmonitor:
    ports:
      - "8080:3001"
      - "1883:1883"  # Embedded MQTT broker

The Docker Configurator has a one-click checkbox for this under section 8.

2. (Optional) Add bridges to public upstreams ​

For each upstream broker (e.g. mqtt.meshtastic.org, regional community brokers), pick MQTT Bridge (forward to/from an upstream broker) in Add Source:

FieldNotes
Parent broker (optional)Pick the mqtt_broker source from step 1 to make this an attached bridge, or leave as "None β€” standalone client proxy" for a pure upstream client. See Two source types for which to pick.
Upstream URLmqtt://mqtt.meshtastic.org (or mqtts://...:8883 for TLS)
Username / PasswordWhatever the upstream needs (e.g. meshdev / large4cats for mqtt.meshtastic.org)
Upstream topicsOne per line, MQTT wildcards allowed (e.g. msh/US/FL/#)
Block topicsOptional β€” drop publishes matching these patterns (e.g. msh/CA/QC/#)
Geographic bounding boxOptional β€” drop position packets outside the box

A standalone bridge is the right starting point when you have no embedded broker (pure upstream monitoring) or when you plan to wire a Meshtastic source's mqttLink directly at the bridge for client-proxy traffic β€” both are also covered in Use-case recipes above.

3. Configure your Meshtastic devices ​

Direct TCP path β€” On the device (via MeshMonitor's Device β†’ MQTT tab, the Meshtastic mobile app, or the CLI):

FieldValue
Enabledtrue
Address<MeshMonitor host LAN IP>:1883
Username / PasswordSame as set on the broker source
RootSame as set on the broker source (default msh)
proxy_to_client_enabledfalse (firmware opens the TCP socket itself)

Client-proxy path β€” On the device:

FieldValue
Enabledtrue
AddressDecorative in proxy mode, but conventionally set to the broker (or bridge upstream) hostname
Username / PasswordDecorative in proxy mode
proxy_to_client_enabledtrue (firmware hands every publish off via the TCP API)

Then on the Meshtastic source in MeshMonitor (Dashboard β†’ Sources β†’ Edit β†’ Bridge MQTT proxy to), pick either an embedded broker or a bridge as the target. The Quick Configure dropdown on the Device β†’ MQTT page does all three of these things (firmware flag, firmware fields, source link) in one click and shows the target type next to the name.

  • Picking a broker routes the device's MQTT traffic into the embedded broker (and from there, optionally onward via any attached bridge). Other LAN clients connected to the broker also see this traffic.
  • Picking a bridge (typically a standalone one) routes the device's MQTT traffic straight to that bridge's upstream connection with no embedded broker in between. Useful when you have no other local MQTT clients.

In both cases MeshMonitor forwards FromRadio.mqttClientProxyMessage payloads to the target's MQTT layer, and injects the target's inbound MQTT traffic back to the device as ToRadio.MqttClientProxyMessage.

If proxy_to_client_enabled is on but no mqttLink is set, a yellow warning banner appears on the Device β†’ MQTT page β€” without it, proxy traffic from the firmware would be silently dropped.

Zero-hop injection ​

Added in 4.6.3

The Zero-hop injection toggle on the broker source ships in 4.6.3 (issue #3084).

Meshtastic's public broker at mqtt.meshtastic.org overwrites the hop_limit field on every packet it re-publishes to its MQTT clients, setting it to 0. Devices that receive a packet via MQTT therefore see "no hops remaining" and skip the RF re-broadcast β€” the firmware enforces a max of 7 hops (10 on older firmware), so without this clamp an MQTT-bridged packet can flood several RF rings before dying out.

If you run a private broker and bridge it to public upstreams, you may want the same behavior. Zero-hop injection is an opt-in toggle on the mqtt_broker source that does exactly this:

  • Disabled (default) β€” packets are forwarded byte-for-byte. Use this for fully private setups where you actually want MQTT-bridged packets to take additional RF hops (small isolated mesh, deliberate fan-out).
  • Enabled β€” the broker decodes each Meshtastic ServiceEnvelope it delivers to a connected client, clamps hop_limit to 0, and re-encodes. hop_start is preserved so receivers can still compute "how far has this travelled". Mirrors Meshtastic's public broker so private deployments behave the same way.

Implementation notes:

  • The clamp only applies to packets the broker delivers to its MQTT subscribers (devices, sidecars, anything that connected to your mqtt_broker listener). The original hop_limit is preserved in:
    • The MeshMonitor database (so hop diagnostics stay accurate)
    • The payload re-published upstream via any attached mqtt_bridge (so the next broker in the chain sees the original value)
  • Topics outside the broker's rootTopic (e.g. non-Meshtastic publishes), non-decodable payloads, and packets that already have hop_limit == 0 are passed through unchanged.

If you're seeing your private broker flood the mesh after attaching a bridge to a public upstream, this toggle is almost certainly what you want.

Comparison: embedded broker vs MQTT proxy sidecar vs node's built-in MQTT ​

ConcernNode's built-in MQTTMQTT Proxy Sidecar (LN4CY)Embedded MQTT Broker (this feature)
Extra containerNoYes (one per node group)No β€” runs inside MeshMonitor
Where credentials liveOn every deviceOn the device (proxy reads them)Once on the broker source; devices share creds
WiFi required on device?YesNo β€” works over Serial/BLEBoth paths supported (direct = WiFi, proxy = any)
FilteringLimited (topic prefix, ignoreMqtt)None β€” passthroughPer-bridge allow/block lists + geo bbox, applied server-side
Server-side ingestionNone (mesh only sees the message after it lands locally)None (sidecar is pure relay)Yes β€” decoded NodeInfo/Position/Text/Telemetry persist under the broker's sourceId
Multiple upstream brokers from one mesh?No (single config field)No (single sidecar instance β‰ˆ single upstream)Yes β€” N bridges, each with independent filter rules
ReliabilityDepends on node WiFiAuto-restart via DockerAuto-restart via Docker; broker survives bridge failures
Recovery on broker outageManual node restartManual sidecar restartmqtt.js auto-reconnect with backoff
Default-deny authPer-devicePer-deviceBroker refuses connections without configured username/password
TLSYes (on device)Yes (on device + proxy)Plain TCP only in v1 (TLS / WSS deferred β€” track in #3003)
Zero-hop injectionn/a (no broker)n/a (passthrough relay)Optional per broker β€” clamp hop_limit to 0 on delivery to match public-broker behavior (details)
Operational visibilityLimited firmware logsDocker logsPer-source packetsIn / packetsIngested / packetsDropped / lastError in /api/sources/:id/status

Plain-English summary ​

  • Node built-in MQTT is fine if you have one node, one upstream broker, reliable WiFi, and no filtering needs.
  • MQTT Proxy Sidecar is the right answer if you mostly want a Serial/BLE node to still reach MQTT, without writing any new MeshMonitor source config β€” the sidecar is essentially a "mobile app, but always on".
  • Embedded MQTT Broker is the right answer when you want selective bridging (e.g. only forward msh/US/FL/PALM-BEACH traffic from mqtt.meshtastic.org, and only re-broadcast your own self-originated traffic), multiple upstreams from one local mesh, server-side ingestion (the bridged traffic shows up as MeshMonitor source data, not just relay-through), or you need devices without WiFi to publish to a broker that other LAN clients can also subscribe to. Both paths (direct TCP + client-proxy) work simultaneously β€” you can mix them on a per-device basis.
  • Standalone MQTT Bridge (no parent broker) is the right answer when you don't need a local broker at all: either because you just want to monitor an upstream broker like mqtt.meshtastic.org without hosting anything, or because you have a single BLE/serial Meshtastic device in proxy_to_client mode whose MQTT traffic should go straight upstream with no intermediate broker. It's a smaller surface area than a broker + attached bridge β€” one source, one outbound TCP connection, no listener port to expose.

Troubleshooting ​

"Client proxy is enabled but no broker is linked" (yellow banner on Device β†’ MQTT) ​

proxy_to_client_enabled is set on the firmware, but the Meshtastic source has no mqttLink. MeshMonitor will silently drop the proxy publishes unless:

  1. You pick an embedded broker from the Quick Configure dropdown on Device β†’ MQTT (one click β€” also stamps the link via PUT); or
  2. You have the MQTT Proxy Sidecar attached to this source's Virtual Node Server, which publishes to its own configured upstream.

Broker has clientCount: 0 but I configured a device ​

  • Confirm port 1883 is exposed on the container (docker port meshmonitor | grep 1883).
  • Confirm the device's mqtt.address resolves to the MeshMonitor host's LAN IP (not localhost from the device's perspective).
  • Confirm the device's MQTT credentials match the broker source's auth.username / auth.password.
  • The broker enforces default-deny auth: connections without configured credentials are rejected. Check the device's MQTT log for "Connection refused: Bad username or password" β€” if firmware logs aren't visible, watch the broker's lastError field on /api/sources/:id/status.

packetsIn climbing, packetsIngested stays low ​

Most upstream traffic and most device-publish traffic is encrypted at the channel-payload level (e.g. LongFast PSK). MeshMonitor doesn't have those PSKs at the broker level, so the packets get republished (they reach other devices and bridges) but not ingested into the DB. packetsDropped counts these. This is normal β€” only decodable packets (NodeInfo, unencrypted Position/Telemetry, etc.) end up in nodes / messages tables under the broker's sourceId.

Bridge reports lastError: "Bad username or password" ​

The credentials configured on the bridge don't match what the upstream accepts for an MQTT subscriber. For mqtt.meshtastic.org, the public subscriber credentials are meshdev / large4cats (Meshtastic public MQTT docs). Other community brokers may use uplink-only credentials that don't work for raw MQTT subscribers; check with the operator.

Deleting the broker source ​

Deleting an mqtt_broker that has dependent mqtt_bridge sources pointing at it auto-detaches the bridges instead of refusing the delete (issue #3134). Each dependent bridge has its brokerSourceId cleared (it becomes a standalone bridge), and any enabled bridge is restarted with the new config; then the broker is removed. Standalone bridges are valid β€” they keep ingesting upstream and can still act as mqttLink client-proxy targets.

If you also want the dependent bridges gone, delete them after the broker, or before β€” bridges don't require a parent.

Pre-4.7 behavior

Releases that shipped before the standalone-bridge feature refused this delete with a 409 Conflict listing the dependent bridges. Repoint or delete those bridges first when running an older version.

Limitations (v1) ​

  • TLS / WSS not supported β€” listener is plain TCP MQTT 3.1.1 only. Deploy behind a TLS-terminating reverse proxy if you need encrypted device β†’ broker.
  • Single shared credential per broker β€” no per-device usernames in v1.
  • No persistence layer for retained messages β€” the broker is ephemeral in-memory (Aedes default).
  • No MQTT 5.0 β€” the firmware speaks 3.1.1 anyway, so this only matters if you have non-Meshtastic MQTT clients that require v5.