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
- 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.
- 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.
- Handle publish errors.
Publishreturns an error immediately when the connection is down. Decide whether to drop, buffer or retry. Recentautopahoversions also offer a queue-backed publish for buffering during outages. - Shut down cleanly. Calling
Disconnectsends a normal DISCONNECT, so the Will is not triggered and subscribers see your explicit status instead. - Log reason codes. MQTT 5 brokers explain disconnects with reason codes; for example
0x97means a quota was exceeded.OnServerDisconnectgives you that code. - 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.



