# Keel MQTT Gateway > **Do not deploy v0.2.41–v0.2.45.** These versions contain known shared- > subscription delivery bugs that can duplicate deliveries and accumulate > excessive Redis inflight state. Use **v0.3.2**; see the > [release and rollout notes](docs/releases/0.3.2.md). [Website](https://keel-iot.eu) · [Documentation](https://keel-iot.eu/docs.html) · [Integrations](https://keel-iot.eu/integrations.html) · [Engineering Blog](https://keel-iot.eu/blog/) A **cloud-native clustered MQTT broker** written in Go, built on [`mochi-mqtt`](https://github.com/mochi-mqtt/server). Keel is designed as a simpler alternative to VerneMQ and EMQX for teams that need horizontal scalability without adopting a symmetric CRDT-based cluster or a BSL-licensed broker. Instead of making every node equal, Keel separates the cluster into: - **Stateless MQTT Edge nodes** (HPA-scalable, terminate client connections) - **Strongly-consistent Core nodes** (StatefulSet, manage cluster metadata via Raft) - **AP Distributed routing** (Olric) - **Direct gRPC data plane** (Zero message payload replication through consensus) --- ## Highlights - **MQTT 3.1.1 / MQTT 5** support - **Core/Edge Architecture:** Strongly consistent control plane (Raft) + eventually consistent data plane (Olric) - **Auth:** per-tenant password, mTLS (X.509 CN + trusted CA pool), and JWT/JWKS (any OIDC-compliant IdP) — credential storage backends (postgres/file) are a fixed set today, no staged plugin interface yet. Two companion device-side agents cover the two real-world tiers: [`keel-cert-manager`](../keel-cert-manager) (Clavex Device PKI, full certificate rotation + revocation) and [`keel-jwt-agent`](../keel-jwt-agent) (generic OAuth2 client-credentials, no real revocation beyond token TTL) - **QoS 1/2 persistence:** Co-located Redis (primary+replica) with automatic Raft-driven primary failover; inflight state persists across the accepting Edge's death and is recovered on the client's next reconnect (any Edge) — real end-to-end cluster-failover recovery isn't validated by an executed test yet (tracked: [keel-iot/keel-mqtt-gateway#14](https://github.com/keel-iot/keel-mqtt-gateway/issues/14)), and a cluster-forwarded publish to an already-partitioned live subscriber has no retry today (same issue) — not the unconditional zero-loss guarantee this used to claim - **Kubernetes-first:** StatefulSet for Core, Deployment + HPA for Edge - **Output plugin architecture:** Publish → Transform → Forward runs out-of-process (`hashicorp/go-plugin`/gRPC sidecar), fully isolated from MQTT delivery - **Observability:** Prometheus metrics + OpenTelemetry tracing, tenant-aware sampling - **Single binary:** (`--role=edge`, `core`, `combined`) - **Apache 2.0 License** (No BSL, no SSPL) --- ## High-Level Architecture ```mermaid flowchart TB classDef edge fill:#E8F5E9,stroke:#2E7D32,color:#000 classDef control fill:#F3E5F5,stroke:#6A1B9A,color:#000 classDef data fill:#E0F7FA,stroke:#00838F,color:#000 classDef persist fill:#FFF3E0,stroke:#EF6C00,color:#000 Client["MQTT Clients"] subgraph EDGE["Edge Layer (Stateless)"] Broker["mochi-mqtt\n\nAuth\nACL Cache\nPlugins\nTopic Match\nRouting"] end subgraph CORE["Core Layer (Stateful)"] subgraph CP["Control Plane"] Memberlist["Memberlist"] Raft["Raft"] FSM["FSM"] Memberlist --> Raft Raft --> FSM end subgraph DP["Data Plane"] Olric["Olric\n\nRouting Table\nPub/Sub\nTTL\nCache"] Redis["Redis\n\nQoS\nOffline Queue\nSession Persistence\nRetained Messages"] end end subgraph Integration Kafka Redpanda HTTP end Client --> Broker Broker --> FSM FSM --> Olric FSM --> Redis Broker --> Kafka Broker --> Redpanda Broker --> HTTP class Broker edge class Memberlist,Raft,FSM control class Olric data class Redis persist ``` --- ## Why not VerneMQ or EMQX? - **VerneMQ** clusters nodes symmetrically using CRDTs (`vmq_swc`), which can produce non-deterministic merges under heavy subscribe/unsubscribe churn and offers no clean separation between "pod ready" and "cluster ready". - **EMQX** moved to **BSL 1.1** from v5.9.0, limiting free commercial usage. Keel adopts the same architectural direction taken by modern distributed systems (EMQX Mria Core/Replicant, RabbitMQ Quorum Queues, NATS JetStream): - Core nodes (stateful) for cluster metadata - Stateless edge nodes for connection termination - Strongly consistent metadata, independent AP data plane --- ## Architecture Details Keel separates responsibilities into independent layers to avoid bottlenecks on the hot path. | Layer | Responsibility | Technology | |--------|---------------|------------| | Transport | MQTT protocol | mochi-mqtt | | Broker | Auth, ACL, plugins, routing | Internal | | Control Plane | Cluster metadata (Session Ownership, Redis Primary) | Raft + Memberlist | | Data Plane | Routing table, cache, pub/sub | Olric | | Persistence | QoS persistence, offline queue, retained | Redis | | Integration | Kafka, HTTP | Internal | ### Data Ownership - **Raft stores only cluster metadata:** Session ownership, cluster configuration, ACL version, Redis primary election. - **Olric stores only derivable state:** Routing table, topic registry, distributed cache, TTL. - **Redis stores only durable MQTT state:** QoS persistence, offline queue, session persistence, retained messages. No MQTT payload is replicated through Raft. No MQTT packet is routed through Olric. The MQTT data plane is always **direct gRPC** between broker nodes. --- ## Node roles | | Core | Edge | |------|------|------| | MQTT Listener | Optional | ✔ | | Raft | ✔ | ✖ | | Memberlist | ✔ | ✔ | | Olric | ✔ | ✖ | | Redis | ✔ | ✖ | | State | ✔ | ✖ | | Kubernetes | StatefulSet | Deployment + HPA | --- ## Data Flows ### 1. CONNECT sequence When a client connects, the edge node claims the session via Raft to guarantee global uniqueness. ```mermaid sequenceDiagram participant C as Client participant E as Edge Node participant R as Raft (Core) C->>E: CONNECT E->>R: ClaimSession(clientID) R-->>E: OK (Ownership Acquired) E->>E: ACL Check (Local Cache) E->>C: CONNACK ``` ### 2. PUBLISH sequence Messages are routed directly between edge nodes using a local cache, falling back to Olric only when necessary. ```mermaid sequenceDiagram participant P as Publisher participant E1 as Edge 1 participant E2 as Edge 2 participant O as Olric (Core) P->>E1: PUBLISH E1->>E1: Check Local Topic Cache alt Subscriber on E1 E1->>P: PUBACK E1->>P_sub: Local Delivery else Subscriber on E2 E1->>E2: Forward via gRPC E2->>P_sub: Remote Delivery E1->>P: PUBACK else Unknown Route E1->>O: Lookup Topic O-->>E1: Return Node List E1->>E2: Forward via gRPC end ``` ### 3. FAILOVER sequence If a core node crashes, Raft elects a new leader and Redis is promoted to avoid QoS data loss. ```mermaid sequenceDiagram participant Core1 as Core 1 (Leader) participant Core2 as Core 2 (Follower) participant Core3 as Core 3 (Follower) participant Redis as Redis Primary Core1--xCore1: CRASH! Note over Core2,Core3: Heartbeat timeout Core2->>Core2: Request Election Core3->>Core2: Vote Core2->>Core2: Elected Leader Note over Core2,Redis: Redis Failover Loop triggers Core2->>Redis: PROMOTE REPLICA (SLAVEOF NO ONE) Redis-->>Core2: OK Core2->>Core3: Update Raft State (New Primary) Note over Core2,Core3: Traffic Continues ``` --- ## Kubernetes Deployment ```mermaid graph TD LB[LoadBalancer / Ingress] --> EDP[Edge Deployment] subgraph Edge Layer EDP --> EP1[Edge Pod 1] EDP --> EP2[Edge Pod 2] EDP --> EP3[Edge Pod N...] end EP1 & EP2 & EP3 --> CS[Core StatefulSet] subgraph Core Layer CS --> CP1["Core Pod 1
Raft + Olric + Redis P"] CS --> CP2["Core Pod 2
Raft + Olric + Redis R"] CS --> CP3["Core Pod 3
Raft + Olric + Redis R"] end ``` --- ## Observability - **Metrics:** Prometheus exposed on `/metrics` (`cfg.MetricsAddr`) — connections, auth duration, message throughput, raft apply duration, forwarder drops, and more (`internal/telemetry/metrics.go`). - **Tracing:** OpenTelemetry via OTLP/gRPC, enabled by setting `OTLP_ENDPOINT`. Sampling is 10% by default, with per-tenant opt-in to 100% (`tracing_enabled` in tenant gateway config). Spans cover connection auth, publish, raft apply, and forwarder retries (`internal/telemetry/tracing.go`). --- ## Quick start Requires Docker and Docker Compose. ```sh docker compose up -d --build ``` The default topology starts a **combined** cluster (Core + Broker in the same process) suitable for development. ### Test it in 30 seconds Open two terminals to test pub/sub: **Terminal 1 (Subscriber):** ```sh mosquitto_sub -h localhost -p 1883 -t "test/topic" -v ``` **Terminal 2 (Publisher):** ```sh mosquitto_pub -h localhost -p 1883 -t "test/topic" -m "Hello Keel!" ``` For production-like deployments (Core / Edge split, TLS, Redis failover), see: - `deploy/docker-compose/README.md` - `docker-compose.core-edge-split.yml` - `docker-compose.tls.yml` --- ## MQTT Conformance Keel currently passes all 10 MQTT 3.1.1 [Eclipse Paho](https://github.com/eclipse-paho/paho.mqtt.testing) conformance tests and 26 of 27 MQTT 5 tests. The remaining MQTT 5 case was independently investigated at packet level — Keel writes the required DISCONNECT (reason 0x93) before an orderly TCP close; the result is classified as a client-side ordering issue in the test harness itself, not a Keel protocol violation. Full methodology, evidence, and the machine-readable report: [`test/conformance/README.md`](test/conformance/README.md). We don't claim "fully MQTT compliant" — that phrase doesn't mean much without the matrix behind it. The conformance suite runs against the real released binary, black-box, with Keel's own auth/ACL policy isolated out (see `--conformance-test`) so what's being measured is the protocol engine, not our own configuration choices. ## Status & Roadmap Keel is currently production-ready for its core feature set. **Validated:** - Reconnect storms (tested up to 10,000 devices, 0 message loss on QoS 1/2) - Redis failover (Split-brain prevention, 0 lost messages on primary crash) - JWKS auto-fetch and key rotation - Core/edge pod kills (leader failover, edge pod restart) on a real K8s cluster - MQTT 5.0 Shared Subscriptions (`$share/group/topic`), exactly-once delivery per group across the whole cluster - Output plugin isolation: a slow/failing Kafka-Redpanda producer can no longer block MQTT delivery (fixed 2026-07-31) - StatefulSet rolling update (Olric cold-start sequencing) — `/readyz` (TLS, Redis, raft leader, local Olric reachability, present since v0.2.3) has been through several real rolling updates on GKE (v0.2.0 → v0.2.5) with no issues observed - HPA edge load metric end-to-end on GKE: `prometheus-adapter` (VictoriaMetrics-backed) registering `custom.metrics.k8s.io`, HPA reading real `keel_edge_load_score` values (`ScalingActive: True`, `ValidMetricFound`) — confirmed on a real GKE cluster 2026-08-03 - Eclipse Paho MQTT conformance: 10/10 on MQTT 3.1.1, 26/27 on MQTT 5 (the one remaining case is a documented client-side harness issue, not a Keel gap — see [MQTT Conformance](#mqtt-conformance) above) **Roadmap / Known Gaps:** - Auth/ACL staged plugin interface (JWT/OAuth/other providers today require code changes, not a plugin) — deliberately deferred - Advanced K8s Operator for automated cluster scaling. --- ## Documentation See: - `CONTRIBUTING.md` - `FEATURE_MATRIX.md` — feature-by-feature verification status (MQTT 3.1.1/5 today; cluster, chaos, and performance scenarios to follow the same evidence-based format) - `ROADMAP.md` — the six-phase plan (functional gaps → feature correctness → distributed correctness → chaos → performance → competitive benchmarking) this matrix feeds into --- ## License Apache License 2.0