Table of Contents

Table of Contents

MetricMQ is a 328 KB C++20 message broker built from scratch and still under development. It features a custom binary wire protocol, Ed25519 message signing enforced at the wire level, exactly-once delivery using sequence IDs with LMDB persistence, Prometheus metrics on every message path, and a native ESP32 Arduino client. This page shows the broker running four demos, each highlighting a different capability with live Prometheus metrics you can watch in real time.

Getting Started with MetricMQ on ESP32

MetricMQ is still in its early beta stage, If you encounter any issues please report it at https://github.com/Saptarshi-max/MetricMQ/issues that will help in making the lib better

Key characteristics:

  • 328 KB binary size
  • Embedded LMDB storage
  • Dual protocol support (RESP + Binary)
  • Runs on ESP32/ESP8266 and desktop platforms
  • 106K msg/s throughput (measured, 10KB messages)

Features

Feature Description
Dual Protocol RESP (Redis-compatible) + Binary Protocol
Protocol Detection Automatic detection on first byte
Pub/Sub Topic-based routing with wildcards
Queue Mode PUSH/PULL with round-robin distribution
Exactly-Once Delivery Sequence IDs with ACK tracking
Persistence LMDB embedded database
Metrics Prometheus endpoint on port 9091
Platforms Windows, Linux, macOS, ESP32, ESP8266

The Demos presented in this Post

[Concepts]  What is pub/sub? Why not just MQTT?
[Setup]     Build the broker on your PC. Verify with redis-cli.
[Demo 1]    Pub/Sub  — ESP32 publishes → PC receives live
[Demo 2]    Exactly-Once — unplug mid-stream → reconnect → missed messages return
[Demo 3]    Fan-Out — one publish → multiple subscribers simultaneously
[Demo 4]    Signed Messages — broker rejects fake data cryptographically

Each demo has its own complete platformio.ini and sketch. Skip to any section independently or work through in order.


What Is a Message Broker?

Already know pub/sub and MQTT? Skip to Setup.

A message broker is a program that sits between devices that produce data (publishers) and devices or services that consume it (subscribers). Think of it like a post office: publishers drop messages off addressed to a topic, the broker sorts and routes them, subscribers collect what matches their subscription.

ESP32 (Publisher)                         PC Terminal (Subscriber)
      │                                           │
      │  publish("sensors/temp", "23.4")          │
      │─────────────► [Broker] ──────────────────►│
      │                                           │
      │                               receives:   │
      │                       "sensors/temp"      │
      │                           "23.4"          │

This pattern — pub/sub — means the ESP32 doesn’t need to know how many things are listening or where they are. It just publishes. New subscribers can join at any time without touching the firmware.

Topics and Wildcards

Topics are strings that work like file paths, using / as a separator:

Subscription What it receives
sensors/temperature Only exactly that topic
sensors/+ sensors/temperature, sensors/humidity — any single segment
sensors/# Everything under sensors/ at any depth
# Every single message on the broker

#

Setting Up the Broker

The broker runs on your PC. Your ESP32 connects to it over your local WiFi. The steps below are verified on Windows 11 with Visual Studio 2022.

What You Need

  • Windows: Visual Studio 2022 Community (free) with “Desktop development with C++” workload
  • Linux: sudo apt install build-essential
  • macOS: xcode-select --install
  • CMake 3.20+cmake.org/download
  • Conan 2.xpip install conan then conan profile detect

Build the Broker

git clone https://github.com/Saptarshi-max/MetricMQ.git
cd MetricMQ
mkdir build
cd build
conan install .. --build=missing -of .
cd ..
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="build/conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release

The first build takes 5–15 minutes while Conan downloads libsodium, LMDB, Boost, spdlog, and Google Benchmark. Subsequent builds use the local cache and are near-instant.

You will see 'pwsh.exe' is not recognized warnings during the build — these are harmless. The CMakeLists.txt has optional post-build scripts that look for PowerShell Core, which is different from standard powershell.exe. They don’t affect any output binaries.

What Gets Built

dir build\Release\*.exe
Binary Purpose
metricmq-broker.exe The broker — start this first, always
metricmq-keygen.exe Ed25519 key pair generator (Demo 4)
binary_pub_only.exe Desktop binary publisher for testing
binary_sub_only.exe Desktop binary subscriber for testing
exactly_once_test.exe ACK/replay automated test suite
signed_publish_test.exe Ed25519 crypto test suite
latency_benchmark.exe p50/p99/p99.9 latency measurement
throughput_benchmark.exe msg/s throughput measurement

Windows Antivirus - Expected False Positive

518

Avast, Defender, and most Windows antivirus tools will quarantine metricmq-broker.exe the first time you run it. This is a false positive — the IDP.Generic detection fires because the binary is unsigned, opens a TCP server socket, and uses a cryptographic library. All three together look suspicious to heuristic scanners even when completely benign.

Fix in Avast: Quarantine → find metricmq-broker.exe → Restore → Menu → Settings → Exceptions → add build\Release\.

Fix in Windows Defender:

Add-MpPreference -ExclusionPath "C:\path\to\MetricMQ\build\Release\"

Start the Broker

.\build\Release\metricmq-broker.exe

Expected output:

MetricMQ Broker v1.0 — Lightweight Message Queue for IoT

Starting broker on port 6379...
Starting metrics server on port 9091...
Press Ctrl+C for graceful shutdown

[info] Metrics server started on http://0.0.0.0:9091/metrics
[info] Broker listening on 0.0.0.0:6379
Broker listening on port 6379

The banner may show garbled Unicode characters on some Windows terminals — cosmetic only, does not affect anything.

Keep this terminal open for all four demos.

Open the Firewall

New-NetFirewallRule -DisplayName "MetricMQ" `
    -Direction Inbound -LocalPort 6379,9091 `
    -Protocol TCP -Action Allow -Profile Any

Install redis-cli and Verify

redis-cli is how you interact with the broker from a terminal — you’ll use it in every demo:

winget install Redis.Redis

Close and reopen PowerShell after installing so the PATH refreshes, then:

redis-cli -p 6379 PING
# Should return: PONG

If you see PONG, the broker is running and reachable. If not, fix this before touching the ESP32 — debugging network problems from a terminal is much easier than from Serial Monitor.

Find Your PC’s IP Address

Your ESP32 sketch needs your PC’s local network IP:

# Windows
ipconfig
# Look for "IPv4 Address" under your WiFi adapter

It will look like 192.168.1.X or 10.0.0.X. Your ESP32 and PC must be on the same WiFi network — same router, same subnet.


Setting Up the ESP32 Library

Installing the Library

Copy the three files from esp32-metricmq/src/ into your PlatformIO project’s lib/MetricMQ/ folder:

your-project/
├── lib/
│   └── MetricMQ/
│       ├── MetricMQ.h
│       ├── MetricMQ.cpp
│       └── library.properties
├── src/
│   └── main.cpp
└── platformio.ini

No lib_deps needed — PlatformIO finds libraries in lib/ automatically.

The platformio.ini for All Demos

[env:4d_systems_esp32s3_gen4_r8n16]
platform = espressif32
board = 4d_systems_esp32s3_gen4_r8n16
framework = arduino
monitor_speed = 115200
monitor_rts = 0
monitor_dtr = 0
build_flags =
    -DCORE_DEBUG_LEVEL=3

monitor_rts = 0 and monitor_dtr = 0 prevent the USB CDC disconnect cycle on Windows that causes the Serial Monitor to repeatedly reconnect after flashing. After uploading, wait 3 seconds before opening the monitor:

pio run --target upload
Start-Sleep -Seconds 3
pio device monitor --port COM8 --baud 115200

The Two-Call Connect Pattern

Every MetricMQ sketch uses the same two-step connect. This is the most important thing to memorise:

// Step 1 — store broker address (does NOT open TCP yet)
client.begin("192.168.1.100", 6379);

// Step 2 — open TCP connection with a stable client ID
// The client ID is how the broker tracks your ACK offset for replay.
// Use the same ID every boot — changing it means starting fresh.
client.connect("my-device-01");

And in every loop():

// The library does NOT auto-reconnect — you must do this
if (!client.isConnected()) {
    client.begin(BROKER_IP, 6379);   // re-set address before reconnecting
    client.connect(CLIENT_ID);
    return;
}

// MUST run every loop() iteration — drives the 60s keep-alive PING
// Long blocking delays will cause the broker to drop the connection
client.loop();

The Callback Signature

When you subscribe to a topic, your callback receives four parameters:

client.subscribe("sensors/#",
    [](const String& topic,    // topic the message arrived on
       const uint8_t* payload, // raw payload bytes
       size_t length,          // payload length
       uint64_t sequence)      // broker-assigned sequence number
    {
        String msg((const char*)payload, length);
        Serial.printf("[MSG] %s → %s (seq=%llu)\n",
                      topic.c_str(), msg.c_str(), sequence);
    }
);

Note: topic is const String&, not const char*. Use .c_str() when you need a raw C string.


Demo 1: Basic Pub/Sub — “Hello, Broker”

What you learn: Publishing from the ESP32, subscribing from your PC, and seeing cross-protocol interop — the ESP32 speaks binary protocol, redis-cli speaks RESP text protocol, the broker routes between them transparently.

Hardware: One ESP32-S3, USB cable, PC running the broker.

src/main.cpp

/*
 * MetricMQ Demo 1 — Basic Pub/Sub
 *
 * ESP32 publishes a simulated temperature reading every 3 seconds.
 * Subscribe on your PC: redis-cli -p 6379 SUBSCRIBE "sensors/#"
 *
 * The interesting part: the ESP32 uses MetricMQ's 16-byte binary protocol.
 * redis-cli uses RESP (Redis text protocol).
 * The broker detects both from the first byte and routes between them.
 * No configuration on either side.
 */

#include <Arduino.h>
#include <WiFi.h>
#include <MetricMQ.h>

const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER_IP = "192.168.1.100";
const char* CLIENT_ID = "esp32-demo1";

MetricMQClient client;
uint32_t msgCount = 0;
uint32_t lastPub  = 0;

void connectWiFi() {
    Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.printf("\n[WiFi] Connected: %s\n", WiFi.localIP().toString().c_str());
}

void connectBroker() {
    Serial.printf("[Broker] Connecting as '%s'...\n", CLIENT_ID);
    client.begin(BROKER_IP, 6379);
    client.connect(CLIENT_ID);
    Serial.println(client.isConnected()
                   ? "[Broker] Connected OK"
                   : "[Broker] FAILED — check IP and firewall");
}

void setup() {
    Serial.begin(115200);
    delay(500);
    Serial.println("\n=== MetricMQ Demo 1: Basic Pub/Sub ===");
    connectWiFi();
    connectBroker();
}

void loop() {
    if (!client.isConnected()) {
        delay(2000);
        connectBroker();
        return;
    }
    client.loop();

    if (millis() - lastPub > 3000) {
        lastPub = millis();
        msgCount++;

        float temp = 20.0f + (float)random(-30, 80) / 10.0f;
        char payload[64];
        snprintf(payload, sizeof(payload),
                 "{\"msg\":%u,\"temp\":%.1f}", msgCount, temp);

        client.publish(String("sensors/temperature"), String(payload));
        Serial.printf("[PUB #%u] sensors/temperature → %s\n", msgCount, payload);
    }
}

How to Run It

Flash the sketch. Open Serial Monitor. In a second PC terminal:

redis-cli -p 6379 SUBSCRIBE "sensors/#"

What to Expect

Serial Monitor:

=== MetricMQ Demo 1: Basic Pub/Sub ===
[WiFi] Connected: 192.168.1.10
[MetricMQ] Connected to 192.168.1.100:6379
[MetricMQ] Client ID: esp32-demo1 | Signing: OFF
[Broker] Connected OK
[PUB #1] sensors/temperature → {"msg":1,"temp":23.4}
[PUB #2] sensors/temperature → {"msg":2,"temp":21.1}

PC terminal:

1) "message"
2) "sensors/temperature"
3) "{\"msg\":1,\"temp\":23.4}"

The ESP32 spoke binary protocol. redis-cli spoke RESP. The broker translated automatically.

What the Broker Metrics Show

curl http://localhost:9091/metrics | Select-String "metricmq_messages"
metricmq_messages_published_total   4
metricmq_messages_delivered_total   4
metricmq_active_connections         2

published == delivered — zero routing loss. active_connections = 2 — one binary publisher (ESP32), one RESP subscriber (redis-cli).


Demo 2: Exactly-Once Delivery — Surviving a Network Drop

What you learn: What happens to messages while the device is offline, and how MetricMQ replays them on reconnect with no gaps and no duplicates.

How It Works

Every message in binary protocol mode gets a sequence number assigned by the broker. The broker stores every message to LMDB and tracks each subscriber’s last acknowledged sequence. When you reconnect with the same client_id, the broker replays from last_ack + 1:

Publishes:  seq=1  seq=2  seq=3  seq=4  seq=5  seq=6  seq=7
                              │
                        Device drops              │
                        (offline)         Device reconnects
                                          same client_id
                                          last ACK was seq=3

Broker: "replay seq=4, 5, 6, 7 for this client"

Device receives: seq=4 (REPLAY) seq=5 (REPLAY) seq=6 (LIVE) seq=7 (LIVE)

The client_id is how the broker identifies you on reconnect. Without a stable ID, replay doesn’t happen.

src/main.cpp

/*
 * MetricMQ Demo 2 — Exactly-Once Delivery
 *
 * Subscribes with a stable client_id. Tracks sequence numbers.
 * On reconnect, the broker replays any messages that arrived while offline.
 *
 * How to test:
 *   1. Flash and open Serial Monitor
 *   2. Start publishing from PC (command in comment below)
 *   3. While messages 5-10 are publishing, press RST or pull USB
 *   4. Reconnect after 5 seconds — watch the REPLAY labels
 *   5. Total == 20, Duplicates == 0 confirms exactly-once held
 *
 * PC publisher:
 *   Windows: 1..20 | ForEach-Object { redis-cli -p 6379 PUBLISH sensors/temperature "{`"reading`":$_}"; Start-Sleep 1 }
 *   Linux:   for i in $(seq 1 20); do redis-cli -p 6379 PUBLISH sensors/temperature "{\"reading\":$i}"; sleep 1; done
 */

#include <Arduino.h>
#include <WiFi.h>
#include <MetricMQ.h>

const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER_IP = "192.168.1.100";
const char* CLIENT_ID = "esp32-demo2-sub";  // MUST be stable across reboots

MetricMQClient client;
uint32_t totalReceived = 0;
uint32_t duplicates    = 0;
uint64_t lastSeq       = 0;
bool     wasOffline    = false;

void onMessage(const String& topic,
               const uint8_t* payload, size_t length,
               uint64_t sequence) {
    totalReceived++;

    if (sequence <= lastSeq && lastSeq != 0) {
        duplicates++;
        Serial.printf("[!!DUPE] seq=%llu (expected > %llu)\n", sequence, lastSeq);
    } else {
        const char* label = wasOffline ? "REPLAY" : "LIVE  ";
        lastSeq = sequence;
        String msg((const char*)payload, length);
        Serial.printf("[%s] seq=%-4llu | %s\n", label, sequence, msg.c_str());
    }

    Serial.printf("         Total: %u | Duplicates: %u\n",
                  totalReceived, duplicates);
}

void connectAndSubscribe() {
    Serial.printf("[Broker] Connecting as '%s' (last seq=%llu)...\n",
                  CLIENT_ID, lastSeq);
    client.begin(BROKER_IP, 6379);
    client.connect(CLIENT_ID);

    if (client.isConnected()) {
        Serial.println("[Broker] Connected — broker will replay missed messages");
        client.subscribe("sensors/temperature", onMessage);
        wasOffline = false;
    } else {
        Serial.println("[Broker] FAILED");
    }
}

void setup() {
    Serial.begin(115200);
    delay(500);
    Serial.println("\n=== MetricMQ Demo 2: Exactly-Once Delivery ===");
    Serial.println("Pull USB or press RST while messages are publishing.\n");

    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.printf("\n[WiFi] %s\n", WiFi.localIP().toString().c_str());

    connectAndSubscribe();
}

void loop() {
    if (!client.isConnected()) {
        wasOffline = true;
        Serial.printf("\n[Broker] Disconnected at seq=%llu — reconnecting...\n",
                      lastSeq);
        delay(2000);
        connectAndSubscribe();
        return;
    }
    client.loop();
}

What to Expect

[LIVE  ] seq=1    | {"reading":1}
         Total: 1 | Duplicates: 0
[LIVE  ] seq=4    | {"reading":4}
         Total: 4 | Duplicates: 0

--- RST pressed here ---

[Broker] Disconnected at seq=4 — reconnecting...
[Broker] Connected — broker will replay missed messages
[REPLAY] seq=5    | {"reading":5}
[REPLAY] seq=6    | {"reading":6}
[REPLAY] seq=7    | {"reading":7}
[LIVE  ] seq=8    | {"reading":8}
...
         Total: 20 | Duplicates: 0

The ScreenShot, of the Platformio Serial Terminal :

What the Broker Metrics Show

# While device is offline:
metricmq_ack_tracking_efficiency    62   ← drops during outage — expected
metricmq_lmdb_stored_messages       8    ← broker kept storing

# After reconnect and replay:
metricmq_ack_tracking_efficiency    100  ← restored after replay ACKs
metricmq_replay_messages_total      4    ← exactly how many were recovered

ack_tracking_efficiency returning to 100 after reconnect is the broker confirming the replay worked and everything was acknowledged.

Replay window: The broker retains the last 100,000 messages in LMDB. At 1 message/second that’s 27 hours of replay coverage — more than enough for typical WiFi outages.


Demo 3: Fan-Out with Wildcards — One Publish, Many Subscribers

What you learn: How one ESP32 publishing to a topic can be received by multiple subscribers simultaneously using wildcard patterns, with no firmware changes.

src/main.cpp

/*
 * MetricMQ Demo 3 — Fan-Out with Wildcards
 *
 * Publishes temperature, humidity, and CO2 to three subtopics every 5 seconds.
 *
 * Open three PC terminals and run:
 *   Terminal A: redis-cli -p 6379 SUBSCRIBE "sensors/env/temperature"
 *   Terminal B: redis-cli -p 6379 SUBSCRIBE "sensors/env/+"
 *   Terminal C: redis-cli -p 6379 SUBSCRIBE "sensors/#"
 *
 * A gets 1 message/cycle. B and C each get 3.
 * The publisher never changes — just add more subscribers.
 */

#include <Arduino.h>
#include <WiFi.h>
#include <MetricMQ.h>

const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER_IP = "192.168.1.100";
const char* CLIENT_ID = "esp32-demo3";

MetricMQClient client;
uint32_t cycle  = 0;
uint32_t lastPub = 0;

void setup() {
    Serial.begin(115200);
    delay(500);
    Serial.println("\n=== MetricMQ Demo 3: Fan-Out with Wildcards ===");
    Serial.println("Open 3 PC terminals (commands in sketch comment).\n");

    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.printf("[WiFi] %s\n", WiFi.localIP().toString().c_str());

    client.begin(BROKER_IP, 6379);
    client.connect(CLIENT_ID);
    Serial.println(client.isConnected() ? "[Broker] Connected" : "[Broker] FAILED");
}

void loop() {
    if (!client.isConnected()) {
        delay(2000);
        client.begin(BROKER_IP, 6379);
        client.connect(CLIENT_ID);
        return;
    }
    client.loop();

    if (millis() - lastPub > 5000) {
        lastPub = millis();
        cycle++;

        float temp = 21.0f + (float)random(-20, 50) / 10.0f;
        float hum  = 45.0f + (float)random(-100, 200) / 10.0f;
        int   co2  = 400 + random(0, 600);

        client.publish("sensors/env/temperature", String(temp, 1));
        client.publish("sensors/env/humidity",    String(hum, 1));
        client.publish("sensors/env/co2",         String(co2));

        Serial.printf("[Cycle %u] temp=%.1f°C  hum=%.1f%%  co2=%d ppm\n",
                      cycle, temp, hum, co2);
    }
}

What to Expect

Terminal A (exact):   1 message/cycle  (temperature only)
Terminal B (+ wild):  3 messages/cycle (temp + hum + co2)
Terminal C (# wild):  3 messages/cycle (everything under sensors/)

Check the fan-out multiplier live:

(Invoke-WebRequest http://localhost:9091/metrics).Content |
    Select-String "delivered|published"
metricmq_messages_published_total   9    ← 3 topics × 3 cycles
metricmq_messages_delivered_total   21   ← A(3) + B(9) + C(9)

Fan-out ratio = 21 / 9 = 2.33. Add a fourth subscriber and watch it climb to ~3.0 without touching the sketch.


Demo 4: Signed Messages — Proving Your Data Is Real

What you learn: Ed25519 message signing on the device. The broker verifies the signature before routing. Unsigned messages to secure/ topics are rejected at the wire level.

Why This Matters

Your ESP32 publishes {"temp":23.4}. Someone on the same WiFi publishes {"temp":-99} to the same topic. Both look identical to your backend. Ed25519 signing closes this gap: your device signs every message with a private key (64 bytes, never transmitted). The broker has the corresponding public key and verifies before routing. A message without a valid signature never reaches any subscriber.

This is authentication, not encryption. The payload is still visible on the wire. Signing proves who sent it and that it wasn’t modified.

Step 1: Generate a Key Pair

.\build\Release\metricmq-keygen.exe "esp32-demo4" "secure/sensors/*"

Output:

Secret key (64 bytes) — device only, never share:
  { 0x4a, 0x3f, ... }

Public key (32 bytes) — register on broker:
  { 0x8c, 0x1d, ... }

The screenshot on your terminal:

Create include/device_secrets.h (add to .gitignore):

// include/device_secrets.h — NEVER commit this file
#pragma once
static const uint8_t SECRET_KEY[64] = { 0x4a, 0x3f, /* ... 64 bytes */ };
static const uint32_t KEY_ID = 1;

Step 2: Register the Public Key on the Broker

In MetricMQ/src/main.cpp, add before broker.run():

uint8_t pk[32] = { 0x8c, 0x1d, /* ... 32 bytes */ };
broker.get_keystore().register_key(1, pk, {"secure/sensors/*"});

Rebuild and restart:

cmake --build build --config Release
.\build\Release\metricmq-broker.exe

src/main.cpp

/*
 * MetricMQ Demo 4 — Ed25519 Signed Messages
 *
 * Prerequisites (complete before flashing):
 *   1. Run metricmq-keygen "esp32-demo4" "secure/sensors/*"
 *   2. Paste secret key into include/device_secrets.h (in .gitignore)
 *   3. Register public key in broker src/main.cpp, rebuild, restart broker
 *
 * To test:
 *   Subscriber: redis-cli -p 6379 SUBSCRIBE "secure/sensors/#"
 *   Spoof:      redis-cli -p 6379 PUBLISH secure/sensors/temperature '{"fake":true}'
 *               → broker prints SIGNATURE_REQUIRED, subscriber gets NOTHING
 */

#include <Arduino.h>
#include <WiFi.h>
#include <MetricMQ.h>
#include "device_secrets.h"

const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER_IP = "192.168.1.100";
const char* CLIENT_ID = "esp32-demo4";

MetricMQClient client;
uint32_t msgCount = 0;
uint32_t lastPub  = 0;

void connectBroker() {
    // setSigningKey MUST come before begin()/connect()
    client.setSigningKey(SECRET_KEY, KEY_ID);
    Serial.printf("[Broker] Connecting in signed mode (key_id=%u)...\n", KEY_ID);
    client.begin(BROKER_IP, 6379);
    client.connect(CLIENT_ID);
    Serial.println(client.isConnected()
                   ? "[Broker] Connected — signed mode active"
                   : "[Broker] FAILED");
}

void setup() {
    Serial.begin(115200);
    delay(500);
    Serial.println("\n=== MetricMQ Demo 4: Signed Messages ===");

    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
    Serial.printf("[WiFi] %s\n", WiFi.localIP().toString().c_str());

    connectBroker();
}

void loop() {
    if (!client.isConnected()) {
        delay(2000);
        connectBroker();
        return;
    }
    client.loop();

    if (millis() - lastPub > 5000) {
        lastPub = millis();
        msgCount++;

        float temp = 22.5f + (float)random(-30, 30) / 10.0f;
        char payload[96];
        snprintf(payload, sizeof(payload),
                 "{\"msg\":%u,\"temp\":%.1f,\"device\":\"%s\"}",
                 msgCount, temp, CLIENT_ID);

        client.publishSigned(String("secure/sensors/temperature"),
                             String(payload));
        Serial.printf("[SIGNED #%u] %s\n", msgCount, payload);
    }
}

What to Expect

Broker console after a legitimate signed message:

[info] Key 1 verified OK — routing secure/sensors/temperature

After the spoof attempt:

[warn] SIGNATURE_REQUIRED — rejecting unsigned publish on secure/sensors/temperature

The subscriber receives nothing from the spoof.

What the Broker Metrics Show

metricmq_signature_verifications_total   8    ← legitimate signed messages
metricmq_signature_failures_total        1    ← the rejected spoof

signature_failures_total should be 0 in a healthy deployment. Any non-zero value means either a device has a corrupted key or someone is probing your broker with unsigned frames.


Prometheus + Grafana: Seeing Your System Live

This is the part most tutorials skip. The broker exposes live metrics on port 9091 from the moment it starts — you don’t enable anything extra. Here’s why it matters and how to use it.

What Value Does This Actually Serve?

Without observability, your IoT system is a black box. You know the broker process is running, but you don’t know:

  • How many devices are currently connected?
  • Are messages actually being delivered, or silently dropped?
  • Has any device been offline long enough to risk losing data?
  • Did someone just inject fake sensor data?
  • Is the broker getting slower under load?

Prometheus collects these numbers continuously. Grafana makes them visible. Together they answer all five questions in real time without SSH’ing into anything.

The Prometheus data (raw format) in the browser, Open up http://localhost:9091/metrics in your broswer when the broker is running: (ScreenShot Below)

The Metrics That Matter

# One-shot snapshot
(Invoke-WebRequest http://localhost:9091/metrics).Content |
    Select-String "^metricmq_"

# Live view, refreshes every 2 seconds
while ($true) {
    Clear-Host
    (Invoke-WebRequest http://localhost:9091/metrics -UseBasicParsing).Content `
        -split "`n" | Where-Object { $_ -match "^metricmq_" -and $_ -notmatch "^#" }
    Start-Sleep 2
}
Metric What it tells you
metricmq_active_connections How many clients are connected right now
metricmq_messages_published_total Total publishes the broker accepted
metricmq_messages_delivered_total Total deliveries that reached a subscriber
metricmq_ack_tracking_efficiency % of binary messages ACKed — target: ~100
metricmq_replay_messages_total Messages replayed from LMDB after reconnects
metricmq_publish_latency_microseconds p50/p99/p99.9 routing latency
metricmq_signature_verifications_total Successful Ed25519 verifications
metricmq_signature_failures_total Rejected/unsigned frames — target: 0
metricmq_lmdb_stored_messages Messages currently persisted in LMDB

Setting Up Prometheus (10 Minutes)

Note: Prometheus is not available via winget — it is distributed as a zip archive from the official GitHub releases page. The commands below download, extract, and configure it in one go.

Step 1: Download and extract Prometheus:

# Run this in PowerShell — downloads Prometheus and extracts to C:\prometheus
$version = "3.4.0"
$url     = "https://github.com/prometheus/prometheus/releases/download/" +
           "v$version/prometheus-$version.windows-amd64.zip"

Invoke-WebRequest -Uri $url -OutFile "$env:TEMP\prometheus.zip"
Expand-Archive -Path "$env:TEMP\prometheus.zip" `
               -DestinationPath "C:\prometheus" -Force

Write-Host "Prometheus extracted to C:\prometheus"

Step 2: Create C:\prometheus\prometheus.yml — open Notepad and save this content to that path:

global:
  scrape_interval: 5s

scrape_configs:
  - job_name: metricmq
    static_configs:
      - targets: ['localhost:9091']

Step 3: Start Prometheus (keep this terminal open):

# Navigate into the extracted folder and start
$version = "3.4.0"
cd "C:\prometheus\prometheus-$version.windows-amd64"
.\prometheus.exe --config.file="..\prometheus.yml"

You should see:

ts=... level=info msg="Server is ready to receive web requests."

Step 4: Open http://localhost:9090 in your browser. Type metricmq_messages_published_total in the query box and press Enter. You should see data points immediately if the broker is running.

If you see “No data”, check that the broker is running and that port 9091 is reachable: curl http://localhost:9091/metrics should return metric lines.

Setting Up Grafana (10 More Minutes)

Note: winget install GrafanaLabs.Grafana does not exist. Use the official MSI installer below.

Step 1: Download and install Grafana:

# Downloads the Grafana MSI and installs it as a Windows service
$grafanaVersion = "12.0.1"
$url = "https://dl.grafana.com/oss/release/grafana-$grafanaVersion.windows-amd64.msi"

Invoke-WebRequest -Uri $url -OutFile "$env:TEMP\grafana.msi"
Start-Process msiexec -ArgumentList "/i $env:TEMP\grafana.msi /quiet" -Wait

Write-Host "Grafana installed. Starting service..."
Start-Service -Name "Grafana"

Grafana installs as a Windows service and starts automatically. If the service doesn’t start automatically, run Start-Service -Name "Grafana" from an elevated PowerShell window.

Step 2: Open http://localhost:3000 in your browser. Default login: admin / admin. Grafana will ask you to set a new password on first login — do that and continue.

Step 3: Add Prometheus as a data source:

  • Left sidebar → Connections → Data Sources → Add data source
  • Select Prometheus
  • URL: http://localhost:9090
  • Click Save & Test — you should see “Data source is working”

Step 4: Create a new dashboard with four panels:

  • Top right → New DashboardAdd visualization
  • For each panel: paste the PromQL query, set the panel type, set the title
Panel 1 — Message Rate
PromQL: rate(metricmq_messages_published_total[1m])
Type: Time series | Shows: messages per second live

Panel 2 — Delivery Efficiency
PromQL: metricmq_messages_delivered_total / metricmq_messages_published_total
Type: Stat | Alert if: < 0.95

Panel 3 — ACK Tracking
PromQL: metricmq_ack_tracking_efficiency
Type: Gauge | Alert if: < 95 for > 60s

Panel 4 — Security Events
PromQL: increase(metricmq_signature_failures_total[5m])
Type: Stat | Alert if: > 0
Panel 1 — Message Rate
PromQL: rate(metricmq_messages_published_total[1m])
Type: Time series | Shows: messages per second live

Panel 2 — Delivery Efficiency
PromQL: metricmq_messages_delivered_total / metricmq_messages_published_total
Type: Stat | Alert if: < 0.95

Panel 3 — ACK Tracking
PromQL: metricmq_ack_tracking_efficiency
Type: Gauge | Alert if: < 95 for > 60s

Panel 4 — Security Events
PromQL: increase(metricmq_signature_failures_total[5m])
Type: Stat | Alert if: > 0

What Each Alert Catches in a Real Deployment

delivery efficiency drops below 0.95: Some messages are not reaching subscribers. Could be a crashed consumer, a network partition, or a topic mismatch. In a building automation system with five consumers on a temperature stream, this dropping from 1.0 to 0.6 means three consumers silently went offline.

ack_tracking_efficiency drops below 95 for more than 60 seconds: A subscriber is falling behind - WiFi instability, device overload, or a bug in the application layer consuming messages too slowly. This fires before any data is actually lost.

signature_failures_total increments: Either a device has a corrupted key after a failed OTA update, or someone on the network is probing the broker with unsigned frames. This is your intrusion detection in a single counter — one alert rule covers the entire fleet.

lmdb_stored_messages grows without replay_messages_total growing: Messages are accumulating in LMDB but no subscriber is claiming them. A device has been permanently offline. At 100,000 stored messages the broker starts compacting and the oldest readings are lost. This gives you advance warning.


How to Navigate the MetricMQ Docs

The official docs live at metricmq-docs.netlify.app. Here’s how to use them efficiently depending on what you’re trying to do.

If You’re Just Getting Started

Start with the Quick Start — ESP32 section on the main page. It shows the minimal setup() and loop() pattern. Then come back to this blog post for the full working sketches — the docs show the pattern, the demos here show the complete code.

If You Want to Understand the Wire Protocol

The Wire Protocol table on the main page defines the exact 16-byte header layout:

Offset 0    : version (0x01)
Offset 1    : command byte
Offset 2–3  : topic length  (uint16 big-endian)
Offset 4–7  : payload length (uint32 big-endian)
Offset 8–15 : sequence number (uint64 big-endian)
Offset 16+  : topic bytes, then payload bytes

This is the ground truth for anyone implementing a client on a new platform. If you’re writing a client for STM32, Raspberry Pi, or any other platform, this table plus the BinaryCommand enum values (SUBSCRIBE=0x01, PUBLISH=0x03, MESSAGE=0x04, etc.) is everything you need.

If You’re Integrating MetricMQ Into an Existing Project

For ESP32/Arduino: Copy the three files from esp32-metricmq/src/ into your project’s lib/MetricMQ/ folder. That’s the complete integration — no package manager, no CMake, no external dependencies beyond arduino-esp32 v2.0.0+ which ships libsodium. Your existing WiFi code, sensor reads, and display code stay exactly as they are. MetricMQ adds three things: begin() + connect() in setup(), client.loop() in loop(), and client.publish() wherever you currently Serial.printf() your data.

For native C++: Include metricmq/pubsub.hpp for RESP or metricmq/binary_pubsub.hpp for the binary protocol. The BinaryPublisher class handles frame construction and Ed25519 signing. The BinarySubscriber handles ACK tracking and replay. Add metricmq as a Conan dependency to your existing conanfile.txt.

If You’re Evaluating for a Production Deployment

Read the Production Risks table on the main page before anything else. The most important entries are: the single global mutex (limits concurrency above ~500 clients), no RESP authentication (any TCP client on port 6379 has full access), no session idle timeout (crashed clients leave zombie threads), and LMDB filling to 1 GB with no compaction or TTL. These are real constraints — understand them before deciding if MetricMQ is the right tool for your scale.

If You’re Running Tests

The Running Tests section on the main page lists every test binary and whether it needs a live broker. Start with signed_publish_test — it needs no broker and confirms your Ed25519 setup is correct. Then exactly_once_test with the broker running to confirm ACK tracking and replay. The persistence_test is the most useful for Demo 2 validation.