IoT Tutorials

Go MQTT Client Tutorial with Eclipse Paho autopaho and MQTT 5

Build a production-style Go MQTT client with paho.golang's autopaho package: MQTT 5 over TLS, automatic reconnects, resubscription, Last Will and graceful shutdown.

Go gopher mascot style illustration of a go mqtt client publishing telemetry to a cloud 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 Go MQTT client is a natural fit for backend services that ingest device data, because Go handles many concurrent connections and long-running processes with little overhead. In this tutorial you will build a Go service with Eclipse Paho's autopaho package that connects over TLS using MQTT 5, publishes telemetry, receives commands and survives network drops with automatic reconnects.

Choosing a Go MQTT Library

There are two Eclipse Paho libraries for Go, and picking the right one saves time later.

Library Import path Protocol Reconnect handling
paho.mqtt.golang github.com/eclipse/paho.mqtt.golang MQTT 3.1.1 Built in
paho.golang (paho) github.com/eclipse/paho.golang/paho MQTT 5 None, single connection
paho.golang (autopaho) github.com/eclipse/paho.golang/autopaho MQTT 5 Automatic, with resubscription hook

The older paho.mqtt.golang library is mature and widely used, but it only speaks MQTT 3.1.1. The newer paho.golang module implements MQTT 5, and its autopaho package wraps the low-level client with connection management: it reconnects with back-off, calls you back when the connection is up so you can resubscribe, and lets you wait for the connection before publishing. For new services, autopaho is the better starting point. The MQTT client libraries guide compares options across other languages.

Project Setup

You need Go 1.21 or newer.

mkdir go-mqtt-demo && cd go-mqtt-demo
go mod init example.com/go-mqtt-demo
go get github.com/eclipse/paho.golang@latest

For the first run, use the public MQTT test broker at broker.connectmq.com. It accepts anonymous connections with any non-empty, unique client ID. Pick a random topic prefix such as connectmq/demo/x9p4tz, and remember that anything published there is readable by everyone.

The Complete Go MQTT Client

Save this as main.go. It reads its configuration from environment variables so the same binary runs against the public broker and a private one.

package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"log"
	"math/rand"
	"net/url"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/eclipse/paho.golang/autopaho"
	"github.com/eclipse/paho.golang/paho"
)

type Reading struct {
	Device      string    `json:"device"`
	Temperature float64   `json:"temperature"`
	Timestamp   time.Time `json:"ts"`
}

func env(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	serverURL, err := url.Parse(env("MQTT_URL", "tls://broker.connectmq.com:8883"))
	if err != nil {
		log.Fatal(err)
	}
	clientID := env("MQTT_CLIENT_ID", "go-demo-x9p4tz")
	prefix := env("MQTT_PREFIX", "connectmq/demo/x9p4tz")
	statusTopic := prefix + "/status"
	commandTopic := prefix + "/cmd/#"
	telemetryTopic := prefix + "/telemetry"

	cfg := autopaho.ClientConfig{
		ServerUrls:                    []*url.URL{serverURL},
		TlsCfg:                        &tls.Config{MinVersion: tls.VersionTLS12},
		KeepAlive:                     30,
		CleanStartOnInitialConnection: false,
		SessionExpiryInterval:         600,
		WillMessage: &paho.WillMessage{
			Topic:   statusTopic,
			Payload: []byte("offline"),
			QoS:     1,
			Retain:  true,
		},
		OnConnectionUp: func(cm *autopaho.ConnectionManager, connAck *paho.Connack) {
			log.Println("connected to broker")
			if _, err := cm.Subscribe(context.Background(), &paho.Subscribe{
				Subscriptions: []paho.SubscribeOptions{{Topic: commandTopic, QoS: 1}},
			}); err != nil {
				log.Printf("subscribe failed: %v", err)
			}
			if _, err := cm.Publish(context.Background(), &paho.Publish{
				Topic: statusTopic, Payload: []byte("online"), QoS: 1, Retain: true,
			}); err != nil {
				log.Printf("status publish failed: %v", err)
			}
		},
		OnConnectError: func(err error) {
			log.Printf("connection attempt failed: %v", err)
		},
		ClientConfig: paho.ClientConfig{
			ClientID: clientID,
			OnPublishReceived: []func(paho.PublishReceived) (bool, error){
				func(pr paho.PublishReceived) (bool, error) {
					log.Printf("command on %s: %s", pr.Packet.Topic, pr.Packet.Payload)
					return true, nil
				},
			},
			OnClientError: func(err error) {
				log.Printf("client error: %v", err)
			},
			OnServerDisconnect: func(d *paho.Disconnect) {
				log.Printf("server sent disconnect, reason code %d", d.ReasonCode)
			},
		},
	}

	if user := os.Getenv("MQTT_USERNAME"); user != "" {
		cfg.ConnectUsername = user
		cfg.ConnectPassword = []byte(os.Getenv("MQTT_PASSWORD"))
	}

	cm, err := autopaho.NewConnection(ctx, cfg)
	if err != nil {
		log.Fatal(err)
	}
	if err := cm.AwaitConnection(ctx); err != nil {
		log.Fatal(err)
	}

	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	expiry := uint32(60)

	for {
		select {
		case <-ticker.C:
			payload, _ := json.Marshal(Reading{
				Device:      clientID,
				Temperature: 20 + rand.Float64()*5,
				Timestamp:   time.Now().UTC(),
			})
			_, err := cm.Publish(ctx, &paho.Publish{
				Topic:   telemetryTopic,
				QoS:     1,
				Payload: payload,
				Properties: &paho.PublishProperties{
					ContentType:   "application/json",
					MessageExpiry: &expiry,
				},
			})
			if err != nil {
				log.Printf("publish failed: %v", err)
			}
		case <-ctx.Done():
			log.Println("shutting down")
			shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()
			_ = cm.Disconnect(shutdownCtx)
			<-cm.Done()
			return
		}
	}
}

Run it:

go run .

In a second terminal, watch the traffic and send a command:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-x9p4tz -t 'connectmq/demo/x9p4tz/#' -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i send-x9p4tz -t 'connectmq/demo/x9p4tz/cmd/led' -m 'on'

How the Code Works

Connection management

autopaho.NewConnection starts a background goroutine that connects, and keeps reconnecting with back-off whenever the connection drops. AwaitConnection blocks until the first connection succeeds or the context ends. That is all the reconnect logic you write; no manual loops are needed.

Resubscribing in OnConnectionUp

OnConnectionUp runs after every successful connection, not just the first. That is the right place to subscribe. With CleanStartOnInitialConnection: false and a SessionExpiryInterval of 600 seconds, the broker keeps the session (subscriptions and queued QoS 1 messages) for ten minutes after a drop, so a quick reconnect resumes the session and the extra subscribe is harmless. After a longer outage, the subscribe recreates it. On the public broker, session expiry is capped at 30 minutes.

Receiving messages

OnPublishReceived is a slice of handlers. Each returns whether it handled the message; this lets you chain handlers, for example one per topic family. Keep handlers fast and hand heavy work to a channel or worker pool, because they run on the client's receive path.

MQTT 5 properties

The telemetry publish sets two MQTT 5 properties. ContentType tells consumers the payload is JSON. MessageExpiry of 60 seconds tells the broker to drop the message if it cannot be delivered within a minute, which is exactly what you want for a temperature that will be superseded anyway. The MQTT 5 features guide explains the other properties.

Last Will and status

The Will message publishes a retained offline on the status topic if the service disappears without a clean disconnect. On every connect, the service publishes a retained online. Any dashboard can subscribe to the status topic and always know whether the service is alive.

Moving to a Private Broker

For production, create a broker in the ConnectMQ portal and use the generated credentials. Client IDs are bound to the issued credentials, so pass the exact one:

export MQTT_URL=tls://broker.connectmq.com:8883
export MQTT_CLIENT_ID=YOUR_CLIENT_ID
export MQTT_USERNAME=YOUR_USERNAME
export MQTT_PASSWORD=YOUR_PASSWORD
export MQTT_PREFIX=acme/site1/gateway1
go run .

The broker's certificate is issued by Let's Encrypt, which Go trusts through the system root store, so TlsCfg only needs a minimum version. In a minimal container image such as scratch or distroless/static, make sure CA certificates are present, otherwise the handshake fails with x509: certificate signed by unknown authority.

Production Checklist for a Go MQTT Client

  1. One client ID per process. Two replicas with the same ID will keep disconnecting each other. Give each replica its own credentials, or use MQTT 5 shared subscriptions for load-balanced consumers on plans that support them.
  2. Use QoS 1 for data you care about. The Free plan supports QoS 0 and 1; QoS 2 is available on paid plans. The QoS levels guide explains the trade-offs.
  3. Handle publish errors. Publish returns an error immediately when the connection is down. Decide whether to drop, buffer or retry. Recent autopaho versions also offer a queue-backed publish for buffering during outages.
  4. Shut down cleanly. Calling Disconnect sends a normal DISCONNECT, so the Will is not triggered and subscribers see your explicit status instead.
  5. Log reason codes. MQTT 5 brokers explain disconnects with reason codes; for example 0x97 means a quota was exceeded. OnServerDisconnect gives you that code.
  6. Keep secrets out of code. Read credentials from the environment or a secret manager, never from the source tree.

Troubleshooting

Error Cause Fix
x509: certificate signed by unknown authority No CA bundle in the container Install ca-certificates or copy the bundle into the image
Connects, then disconnects every few seconds Duplicate client ID Ensure each instance has a unique ID
not authorized reason code Wrong credentials or client ID Copy all three values from the portal again
No commands received Subscribed before connection, or topic typo Subscribe inside OnConnectionUp; check wildcards
publish failed during outages Connection down Expected; buffer or retry as needed

For the Paho library reference, see the paho.golang documentation.

Other Backend Languages

The same patterns, a stable client ID, resubscribe on connect, Last Will and TLS, apply everywhere. Compare with the Rust rumqttc tutorial and the Java MQTT client tutorial if your stack is mixed.

Run Your Go Services on ConnectMQ

A managed MQTT broker lets your Go services focus on business logic while someone else runs the broker, certificates and upgrades. ConnectMQ supports MQTT 5 with session expiry, message expiry and reason codes, so everything in this tutorial works unchanged. Create a free Serverless broker at app.connectmq.com, no credit card required, and point MQTT_URL at it.

Frequently Asked Questions

Which Go MQTT library should I use?

For new projects, github.com/eclipse/paho.golang with the autopaho package, because it supports MQTT 5 and manages reconnects. Use paho.mqtt.golang if you need MQTT 3.1.1 compatibility with existing code.

Does autopaho resubscribe automatically after a reconnect?

It calls OnConnectionUp after every connection. Subscribe there, and combine it with a session expiry interval so the broker can also keep the subscription across short outages.

How do I connect a Go MQTT client over TLS?

Use a tls:// server URL with port 8883 and set TlsCfg. For brokers with publicly trusted certificates, the system root store is enough.

Can I run several instances of the same Go service?

Yes, but each instance needs its own client ID. For load-balanced consumption of the same topics, use MQTT 5 shared subscriptions on a plan that supports them.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

How to Store MQTT Data in InfluxDB with Python

Build a Python bridge with paho-mqtt and influxdb-client that subscribes to a broker, turns topic levels into InfluxDB tags, writes numeric JSON values as fields in batches and handles bad payloads and type conflicts.

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.