Skip to main content
High-availability clustering is in beta. It has been tested by repeatedly killing, restarting, and isolating nodes under load, but not yet in production deployments, at production message rates, or across rolling upgrades. Its behavior and configuration options can change in a later release. Test it with your own workload before you rely on it in production, and include your broker version when you report an issue.

Keep messaging available when a node fails

A single broker is a single point of failure. High-availability clustering (beta) runs several Coreflux MQTT Broker nodes as one system. Clients connect to any node, and a message published on one node reaches subscribers on every node. If a node stops, the others keep serving clients and take over its work. A QoS 1 or 2 message is acknowledged to the publisher only once it is stored on a majority of the nodes responsible for it, so losing a minority of the cluster does not lose messages the broker has already accepted.
Like a team that keeps shared minutes. One member chairs the meeting, and every decision is written into the minutes of most members before it counts. If the chair leaves, the others pick a new one and carry on from the same minutes.

When to use clustering

Use clustering when you need:
  • MQTT service that continues when one broker stops, crashes, or is restarted for maintenance;
  • QoS 1/2 messages that survive the loss of a node after the broker has acknowledged them; or
  • one MQTT endpoint served by several broker nodes, on fixed machines or as Kubernetes pods.
Run 3 or 5 nodes. A cluster keeps working as long as a majority of its nodes are up: 2 of 3, or 3 of 5. Clustering requires a license that grants the cluster feature; with clustering enabled and no such license, the broker logs a warning and runs standalone.
Clustering is broker configuration, not a LoT Connector. To exchange topics with another, separate MQTT broker (including non-Coreflux brokers), use an MQTT Bridge instead.

Setting up a cluster takes the same steps on every node: a service user for traffic between nodes, a Cluster section in the broker configuration, and durability turned on. Then load your LoT on any node, and the cluster replicates it to the others. See LoT in a cluster.

Quick Start

1

Create the cluster service user on every node

Nodes exchange messages with each other over MQTT, using a dedicated MQTT user. Create the same user, with the same password, on every node, for example with the -addUser broker command:
Only this user may send cluster traffic between nodes.
2

Configure the cluster on every node

Add a Cluster section to the broker configuration of each node, or set the matching COREFLUX_CLUSTER_* environment variables (environment variables override the file). Keep everything identical except NodeName, and turn durability on as well (see Durability and clustering).
List the peers’ addresses when automatic discovery is not available, which is the case in most cloud networks and across subnets:
3

Start the nodes

Start all nodes. On a first start, the nodes wait up to BootstrapGraceSeconds (30 s by default) to make sure no cluster is running yet, then form a new one and elect a leader. Later restarts rejoin the existing cluster at once.
4

Verify the cluster

Call GET /cluster on the operations port of any node (9100 by default):
Every node names the same leadership.leader, membersTotal equals your number of nodes on every node, and on the leader membersAlive equals membersTotal.

How a cluster works

One leader, automatic membership

The nodes elect one leader. The leader admits new nodes, removes dead ones, and decides which node is responsible for which part of the message traffic. Every node, leader or not, serves MQTT clients in the same way.
  • Elections are automatic. When the leader is lost, the remaining nodes elect a new one within a few election timeouts, typically under a second. That includes a leader that is silent: frozen, or cut off so its cluster port no longer answers.
  • Nodes keep their identity. A node is recognised by its NodeName, not by its IP address. A node that restarts on a new address, or on another node’s old address, is recognised and its old address is dropped at once.
  • Restarts rejoin. Each node remembers its cluster membership on disk and rejoins its peers after a restart, including when every node restarts at the same time.
  • Dead nodes are removed. A node that stays unreachable for MemberRemovalGraceSeconds (5 minutes by default) is removed from the membership, so it no longer counts when the cluster checks for a majority. With kubernetes discovery, a pod that the headless Service no longer lists, and whose cluster port fails a TCP probe, is removed after MemberAbsentFromDiscoverySeconds (2 minutes, 120 seconds, by default). A new pod is found within seconds. A node is added back when it returns.
  • LoT changes go through the leader. Send them to any node: a follower passes them to the leader. Other commands, such as user management, run on the node that receives them. See LoT in a cluster.
  • No accidental second cluster. A node that starts with no cluster state first asks its peers whether they already belong to a cluster, and joins that cluster instead of starting a new one.

Partitions spread the work

QoS 1/2 traffic is split into partitions (32 by default). A publisher’s messages always belong to the same partition, chosen from its client id. Each partition has one owner, the node that accepts its writes, and copies on other nodes (3 copies by default, owner included). The leader spreads the owners evenly across the nodes. When a node fails, its partitions move to nodes that already hold their copies, so no accepted message is left behind.

QoS 1 and 2 are acknowledged once they are safe

When a client publishes at QoS 1 or 2, the broker stores the message on a majority of its partition’s copies (2 of 3) before it sends the PUBACK (QoS 1) or PUBREC (QoS 2). A client connected to a node that does not own its partition is served the same way: that node passes the message to the owner and acknowledges it when the owner confirms. If a majority cannot be reached in time (ReplicationTimeoutMs, 5 s by default), the broker rejects the publish instead of acknowledging a message it cannot keep safe, and does not deliver it: While replication is still starting, that 0x97 refusal uses the reason text “Cluster starting” and lasts at most 30 seconds. Retry; the node accepts the publish once replication is running.
Use MQTT 5 for QoS 1/2 publishers. MQTT 3.1.1 has no way to report a rejected publish: a 3.1.1 client receives a normal acknowledgement for a message the cluster did not accept. MQTT 5 clients see the reason code and can retry.

Subscribers on any node receive every message

A message published on one node is delivered to matching subscribers on every node:
  • QoS 1/2 messages are queued on disk for each other node and re-sent until that node confirms delivery. A node that is down receives its messages when it returns, including messages for its offline persistent sessions.
  • QoS 0 messages are sent to the other nodes once, without storage. A node that is down misses them, as with any QoS 0 message.

Failover

When a node fails, its partitions move to surviving nodes that already hold their copies. The new owner first catches up from the other copies, then re-delivers the messages of the last ReplicationReplaySeconds (5 minutes by default) for those partitions, so subscribers do not miss messages the failed node had accepted but not yet passed on. Subscribers can therefore receive some messages twice after a failover. With ReplicationDedupDelivered on (the default), a node drops a replay copy of a quorum-durable message it has already delivered. Set it to false to deliver every copy. During a failover, publishes to the affected partitions are rejected with 0x97 until the new owner is ready. A new leader is elected within a few election timeouts, typically under a second, including when the lost node is silent. Expect about 15 seconds after a node failure before that owner is ready, and up to about 25 seconds when the failed node was the leader. Publishes to partitions owned by the surviving nodes continue meanwhile. Clients that retry on 0x97 lose nothing.

Sessions follow the client

With durability enabled, a client with a persistent session that reconnects to another node gets its subscriptions back. Messages already queued for it while it was offline stay on the node it was connected to, and are delivered only if it reconnects to that node. See Limitations.

Durability and clustering

Clustering and MQTT state durability protect different parts of a message’s path, and a production cluster needs both:
  • Clustering protects the message after the publisher’s acknowledgement: it is stored on a majority of nodes and survives the loss of one of them.
  • Durability protects the subscriber side on each node: persistent sessions, their subscriptions, the messages queued for them, and retained messages survive a restart of that node.
Enable durability on every node of a cluster. Clients without persistent sessions (MQTT 3.1.1 clean session, MQTT 5 session expiry 0) are not affected either way.

LoT in a cluster

The cluster runs LoT as one logical broker. A client can connect to any node and sees the same definitions, the same shared state, and the same results. A broker that isn’t clustered behaves as before.

The three rules

  1. Same definitions everywhere. Every node runs the same version of every action, model, connector, rule, Python script, and panel. Changes go through the leader and are replicated to every node.
  2. One place per piece of work. A message runs LoT on the node it entered. A client event runs on the node that holds the connection. Timers, startup, and device connections run on one owner node that the cluster picks.
  3. A message from a peer never triggers LoT. It still reaches that node’s subscribers and caches, but the action that reacts to it runs once, where the message came in.

Where LoT runs

Where connectors run

A MODBUS_SERVER on a serial port is an exclusive connection: only its owner opens the port. A TRIGGER or STORE IN on a node that doesn’t own the connector is sent to the owner and runs there. A FETCH needs the answer where it was asked, so on another node it fails with a message that names the owner. Use an event that writes to a DESTINATION_TOPIC instead.

Pin a connector

To keep an exclusive connector on the node that is cabled to the device, set CLUSTER_NODE in CONNECTOR_CONFIG (ROUTE_CONFIG remains a supported alias) to that node’s NodeName:
While the pinned node is down, the connector runs nowhere. Leave CLUSTER_NODE out to let the cluster place and balance it.

Change definitions

Send LoT commands (-addAction, -removeConnector, -loadProject, MCP tools, the HUB) to any node. A follower passes the command to the leader, which validates and commits it. The follower answers once its own node has applied the change.
  • A command that fails changes nothing on any node. A project load is applied as one change.
  • Changes are refused, with the reason, while a majority of nodes can’t be reached, or while a live node runs a version without replicated LoT. Upgrade every node before you change LoT. lot.mixedMode in GET /cluster shows when this is the case.
  • Users and credentials are not replicated. Create them on every node: the node that receives a command checks your permission locally.
First start. Until the first change is committed, each node runs its own LoT. Once the leader has been stable for a few seconds, or on the first LoT command, the leader’s definitions become the cluster’s. A node whose files differ drops what the cluster doesn’t have and saves each definition it removes or replaces under lot-backup/<timestamp>/<kind>/<name>.lot. A node that was away catches up when it returns: it applies what it missed, removes what the cluster removed, and backs up the files it drops.

Owners and failover

Each timer, ON START action, exclusive connector, and scheduled event has one owner and one standby node. Every node agrees on the map, and the cluster spreads the work over the live nodes. Plan on about 10 seconds without a timer or device connection when its owner fails. After a takeover:
  • A timer starts a new cycle, and runs missed during the takeover aren’t made up.
  • DATE_TIME timers use the node’s local time, so set the same time zone on every node.
  • Python module globals, LLM conversations, and the OT publish-on-change state start fresh on the new owner.
  • The node that loses a connector doesn’t clear its tag topics.

ON START and KEEP

ON START runs once per cluster session, on the action’s owner, and again when the action is redeployed with different code.
  • A session starts when the cluster forms from nothing and lasts while any of its members is alive. Restarting one node, or all but one, runs nothing. Restarting every node starts a new session.
  • An ON START action runs at most once per session and code version, even if its node fails before it runs.
  • While a definition fails to apply on a node, ON START actions wait on that node.
KEEP TOPIC values are sent to every node’s cache, without reaching subscribers or triggering LoT there.
  • Values are eventually consistent: when two nodes keep the same topic at about the same time, the later write wins. A normal publish to the topic replaces a kept value.
  • A node that was down gets the values when it returns, and a node keeps its values across a restart. Each value can be up to 1 MB, with up to 100,000 kept topics per node.

Server connectors

A server connector (Modbus server, OPC UA server, REST API server, video output) runs on every node, so a load balancer or Kubernetes Service can send a client to any of them. Every node serves the current value of every tag.
  • OPC UA behind one name. Set WITH ADVERTISED_HOST "opcua.example.com" in OPCUA_SERVER_CONFIG (a host name or address, no scheme, port, or path). To present one identity, put the same PKCS#12 certificate with its private key in <PKI_DIR>/own/private on each node, or import it with -connectorCertificates import. In a container, set PKI_DIR to a writable folder.
  • Failed listeners are reported. GET /cluster lists every connector listener under routeListeners with its state, also on retained $SYS/Coreflux/Cluster/RouteListeners and as coreflux_route_listener_up on /metrics. A failed bind isn’t retried.
  • Open every port. The Service or load balancer must list every server connector port, including ones you add later. MODBUS_SERVER defaults to port 502, which a non-root container can’t bind: map it to an unprivileged port.

What is guaranteed

  • A LoT run is at most once per message. If a node fails after a message is accepted but before LoT runs, that run is lost. Failover replay delivers the message again but doesn’t run LoT again.
  • LoT output isn’t quorum-durable. It’s forwarded to the other nodes over the reliable link.
  • ON CHANGE compares against the last value this node saw. There is no cluster-wide order per topic.
  • QoS 1 redelivery runs an ON TOPIC action twice, as on a single broker.
  • Definitions match on every node once a change is applied. Until then, lot in GET /cluster shows which node is behind and why.

Configuration reference

Set these options in the Cluster section of the broker configuration, or with the listed environment variables (environment variables win). Use the same values on every node, except NodeName, AdvertisedHost, and BindIP.

Identity and discovery

Network

Replication

Membership and startup

The broker checks these values at start. If one is invalid, clustering does not start, the log names the problem, and GET /cluster reports enabled: false.

Ports and network

Keep the cluster port on a private network. Traffic on port 9200 is neither encrypted nor authenticated. SharedSecret signs the messages exchanged between nodes, but not this port. Allow it only between cluster nodes.

Delivery guarantees

Design consumers to tolerate duplicates, for example with a message id or sequence number in the payload. For new designs, QoS 1 gives the same guarantee as QoS 2 in a cluster, with one round trip less.

Operations

Starting, restarting, and scaling

  • First start: expect about BootstrapGraceSeconds (30 s) before the cluster forms. Until then, GET /cluster reports enabled: false or no leader.
  • Restarts: a node rejoins with its previous identity and data. Restart one node at a time, and wait for GET /cluster to show it back before restarting the next.
  • Adding a node: start it with the same cluster settings and a new NodeName, and add it to Seeds on the other nodes if you use seeds. The leader admits it and moves some partitions to it.
  • Removing a node: stop it. After MemberRemovalGraceSeconds it leaves the membership and its partitions stay on the remaining nodes. In Kubernetes, a pod the headless Service no longer lists leaves after about 2 minutes (MemberAbsentFromDiscoverySeconds, 120 seconds by default), once its cluster port fails a TCP probe.
  • Upgrades: all nodes of a cluster must run the same broker version. To upgrade a cluster from 2.14 or earlier, stop all nodes and upgrade them together. LoT changes are refused until every node runs a version that replicates LoT.

Monitoring

Watch these values; on a healthy cluster they stay at the healthy value: Use MQTT Explorer or any MQTT client to subscribe to $SYS/Coreflux/Cluster/#.

Best practices

  • Run 3 or 5 nodes, and spread them across failure domains (hosts, racks, zones).
  • Set SharedSecret, and keep the cluster port on a private network.
  • Enable durability on every node.
  • Use MQTT 5 for QoS 1/2 publishers, and retry on 0x97.
  • Make consumers tolerate duplicates.
  • Give each node a stable NodeName and persistent storage for DataDir and the durability data.
  • Deploy LoT through any one node and let the cluster place it; pin an exclusive Connector to a node with CLUSTER_NODE only when the device is cabled to that node (see LoT in a cluster).
  • Create the same users on every node, and use the same time zone and synchronised clocks.
  • Don’t run 2 nodes: losing either one stops QoS 1/2 publishing, because 1 of 2 is not a majority.
  • Don’t set ReplicationFactor to 1 when you need messages to survive a node failure.
  • Don’t change PartitionCount on a running cluster.
  • Don’t restart several nodes at once on a 3-node cluster: QoS 1/2 publishing pauses until a majority is back.

Limitations

Offline queues stay on their node. A persistent session’s subscriptions follow a client to another node (with durability), but messages already queued for it stay on the node it was connected to, and are delivered only if it reconnects there.
Users and credentials are kept per node. Create the same users on every node. LoT definitions, including Rules, are replicated; users are not.
Retained messages are kept per node. Each node keeps its own copy of retained messages. A node added later does not receive retained messages published before it joined, and a node that was down misses retained QoS 0 messages published meanwhile.
At least once, not exactly once. After a failover, subscribers can receive some messages twice, at QoS 1 and QoS 2.
MQTT 3.1.1 publishers cannot see rejected publishes. When the cluster cannot reach a majority, a 3.1.1 client still receives a normal acknowledgement for a message that was not accepted. Use MQTT 5 for QoS 1/2 publishers.
One publishing connection is limited by the time to reach a majority. Each QoS 1/2 message waits for its majority before the broker reads the same connection’s next message. Total throughput grows with the number of publishing connections. A single connection that publishes at high rate is limited to roughly one message per round trip between nodes.
Shared subscriptions are per node. A shared subscription group includes only the clients connected to the same node; each node delivers a message to one member of its own group. See shared subscriptions.
Agents are not replicated. An agent (DEFINE AGENT) exists only on the node where you defined it. It is not copied to the other nodes, does not move when a node fails, and a -removeAgent on one node does not remove it from the others. Define an agent on every node that should run it. The LLM Connector an agent uses is an ordinary Connector and is replicated.
Agents inside a Thing. Currently:
  • The leader deploys a Thing (-addThing). Its definition and its connectors, models, actions, rules, and panels replicate to every node.
  • A DEFINE AGENT inside the Thing is not replicated. It currently runs only on the node that was leader when the Thing was deployed, which is not necessarily the node that received the command.
  • The agent’s conversation memory stays on that node and is never replicated.
  • The agent does not move on failover. If its node is down, the agent is unavailable until that node returns.
  • On the other nodes the Thing still lists the agent, but the agent is not running there, so Thing health on those nodes may show it as not ok. Its status topics are visible on every node.

Next Steps

MQTT State Durability

Keep sessions, queued messages, and retained messages across restarts on every node.

Operational Endpoints

Monitor each node with /cluster, /info, and Prometheus metrics.
Last modified on October 26, 2026