Skip to main content

Command Palette

Search for a command to run...

The Journey to Running Echo and MQTT

Updated
6 min read
The Journey to Running Echo and MQTT

Hello, I'm Ganesh. I'm working on FreeDevTools online, currently building one place for all dev tools, cheat codes, and TLDRs — a free, open-source hub where developers can quickly find and use tools without any hassle of searching all over the internet.

Have you ever wanted to build an application that needs to serve a web page while handling real-time IoT messages simultaneously? Maybe you're building a dashboard that needs to display live sensor data. You've got a web framework for the API and dashboard, like Echo, and an MQTT broker for the device messages, like Mochi-MQTT.

The natural question is: "Can I run both of these from the same Go program?"

The answer is a resounding yes, and it's one of the things Go excels at. But as with any real-world coding, the path from "idea" to "it works" can have a few interesting detours. Let's walk through the journey, including the "gotchas" and how to solve them.

The Goal

Our mission is to create a single Go binary that does two things:

  1. Runs an Echo web server on port :8080 to handle HTTP requests.

  2. Runs a Mochi-MQTT server on port :1883 to handle IoT device connections.

The Obvious First Attempt

After setting up our project (go mod init my-app) and getting our dependencies (go get ...), our first instinct is to write a main function that just... starts both servers.

// This code will NOT work as intended!
package main

import (
    "log"
    "net/http"

    "github.com/labstack/echo/v4"
    mqtt "github.com/mochi-mqtt/server/v2"
    "github.com/mochi-mqtt/server/v2/listeners"
)

func main() {
    // --- MQTT Server Setup ---
    mqttServer := mqtt.New(nil)
    tcp := listeners.NewTCP(listeners.Config{ID: "mqtt-tcp", Address: ":1883"})
    err := mqttServer.AddListener(tcp)
    if err != nil {
        log.Fatal(err)
    }

    // --- Echo Server Setup ---
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, Echo!")
    })

    // --- Start Servers ---

    // 1. Start the MQTT server
    log.Println("Starting MQTT server on :1883")
    err = mqttServer.Serve() // <-- PROBLEM #1
    if err != nil {
        log.Fatal(err)
    }

    // 2. Start the Echo server
    log.Println("Starting Echo server on :8080")
    e.Logger.Fatal(e.Start(":8080")) // <-- PROBLEM #2
}

If you run this, you'll see "Starting MQTT server on :1883" in your console... and then nothing else. Your Echo server will never start.

Why? Because mqttServer.Serve() is a blocking call. It's designed to run forever, listening for connections. Your program's execution hits that line and never moves past it. The e.Start(":8080") line is, for all practical purposes, unreachable.

The Solution

This is the exact problem goroutines were made for. A goroutine is a lightweight thread of execution that you can launch in the background.

The fix is incredibly simple. We just tell Go to run the first server in a goroutine by adding one simple keyword: go.

// ... inside main() ...

// 1. Start the MQTT server IN A GOROUTINE
log.Println("Starting MQTT server on :1883")
go func() {
    err := mqttServer.Serve()
    if err != nil {
        log.Fatal("MQTT server error:", err)
    }
}()

// 2. Start the Echo server (this blocks, which is now what we want!)
log.Println("Starting Echo server on :8080")
e.Logger.Fatal(e.Start(":8080"))

Here's why this works:

  1. go func() { ... }() launches the MQTT server in the background. The main function doesn't wait for it to finish and immediately moves to the next line.

  2. We also wrap the call in an anonymous function (func() { ... }()) so we can still check for a fatal err inside that goroutine.

  3. The e.Logger.Fatal(e.Start(":8080")) call is not put in a goroutine. This is critical. It blocks, just like before, but now that's a good thing. It keeps the main function alive. If main were to exit, the entire program (including all its background goroutines) would shut down.

Now, when you run your program, you'll see logs for both servers starting up!

Debugging Adventure: "Not Authorized"

We're running! We're champs! Let's connect our client. We open MQTT Explorer, point it to localhost:1883, and click connect...

Connection failed. "Not authorized."

What gives? The server is running, but it's rejecting us. This is a classic "it's not a bug, it's a feature" moment. Mochi-MQTT is secure by default. It doesn't allow anonymous connections unless you explicitly tell it to.

Our first few attempts (like trying to find an "AllowAnonymous" flag) might fail. The correct way, as shown in the Mochi-MQTT documentation, is to add an authentication hook.

We need to set up a "ledger" of allowed users. Let's create one user: my-user with the password my-password.

This requires importing a new package: github.com/mochi-mqtt/server/v2/hooks/auth. Then, we add a new block of code right after creating the server.

Here is the final, complete, and working code.

The Complete, Working main.go

package main

import (
    "log"
    "net/http"

    "github.com/labstack/echo/v4"
    mqtt "github.com/mochi-mqtt/server/v2"
    "github.com/mochi-mqtt/server/v2/hooks/auth" // <-- 1. Import the auth hook
    "github.com/mochi-mqtt/server/v2/listeners"
)

func main() {
    // --- MQTT Server Setup ---
    mqttServer := mqtt.New(nil)

    // --- 2. THIS IS THE AUTHENTICATION BLOCK ---
    // Add the auth hook to define our allowed users.
    err := mqttServer.AddHook(new(auth.Hook), &auth.Options{
        Ledger: &auth.Ledger{
            Auth: auth.AuthRules{
                // Add our user
                {Username: "my-user", Password: "my-password", Allow: true},
            },
            ACL: auth.ACLRules{
                // Allow our user to read/write to all topics (#)
                {Username: "my-user", Filters: auth.Filters{"#": auth.ReadWrite}},
            },
        },
    })
    if err != nil {
        log.Fatal("failed to add auth hook:", err)
    }
    // --- END OF AUTH BLOCK ---

    // Create the TCP listener
    tcp := listeners.NewTCP(listeners.Config{ID: "mqtt-tcp", Address: ":1883"})
    err = mqttServer.AddListener(tcp)
    if err != nil {
        log.Fatal("failed to add listener:", err)
    }

    // --- Echo Server Setup ---
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, this is the Echo Server!")
    })

    // --- Start Servers ---

    // 1. Start the MQTT server in a goroutine
    log.Println("Starting MQTT server on :1883")
    go func() {
        err := mqttServer.Serve()
        if err != nil {
            log.Fatal("MQTT server error:", err)
        }
    }()

    // 2. Start the Echo server in the main goroutine (blocking)
    log.Println("Starting Echo server on :8080")
    e.Logger.Fatal(e.Start(":8080"))
}

Now, run go run main.go. You'll see both servers start.

  • Test Echo: Open your browser to http://localhost:8080. You'll see "Hello, this is the Echo Server!"

  • Test MQTT: Open MQTT Explorer. Connect using localhost, port 1883, username my-user, and password my-password.

Success! You are now connected.

Key Takeaways

Running multiple servers in Go is simple, but you have to remember two key things:

  1. Blocking Calls: Most servers run a blocking Serve() or Start() loop.

  2. Goroutines: Use the go keyword to run all but one of your servers in the background. Let the last one block the main function to keep your app alive.

  3. Read the Docs: Security isn't an afterthought. When you get "Not authorized," it's almost always a sign that you need to read the library's authentication guide.

Now that we have a fully functional web server and MQTT broker, the next logical step is to get a real device, like an ESP32, to connect and start publishing sensor data. Happy coding!

FreeDevTools

I’ve been building for FreeDevTools.

A collection of UI/UX-focused tools crafted to simplify workflows, save time, and reduce friction in searching tools/materials.

Any feedback or contributions are welcome!

It’s online, open-source, and ready for anyone to use.

👉 Check it out: FreeDevTools
⭐ Star it on GitHub: freedevtools

More from this blog

Ganesh Kumar

23 posts