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.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.
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 Only this user may send cluster traffic between nodes.
-addUser broker command: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).- Seeds (typical production)
- Kubernetes
- Multicast (same LAN)
- Environment variables (Docker)
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. Withkubernetesdiscovery, a pod that the headless Service no longer lists, and whose cluster port fails a TCP probe, is removed afterMemberAbsentFromDiscoverySeconds(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.
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 lastReplicationReplaySeconds (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.
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
- 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.
- 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.
- 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, setCLUSTER_NODE in CONNECTOR_CONFIG (ROUTE_CONFIG remains a supported alias) to that node’s NodeName:
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.mixedModeinGET /clustershows 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.
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_TIMEtimers 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 STARTaction 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 STARTactions 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"inOPCUA_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/privateon each node, or import it with-connectorCertificates import. In a container, setPKI_DIRto a writable folder. - Failed listeners are reported.
GET /clusterlists every connector listener underrouteListenerswith its state, also on retained$SYS/Coreflux/Cluster/RouteListenersand ascoreflux_route_listener_upon/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_SERVERdefaults 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 CHANGEcompares against the last value this node saw. There is no cluster-wide order per topic.- QoS 1 redelivery runs an
ON TOPICaction twice, as on a single broker. - Definitions match on every node once a change is applied. Until then,
lotinGET /clustershows which node is behind and why.
Configuration reference
Set these options in theCluster 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
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 /clusterreportsenabled: falseor no leader. - Restarts: a node rejoins with its previous identity and data. Restart one node at a time, and wait for
GET /clusterto show it back before restarting the next. - Adding a node: start it with the same cluster settings and a new
NodeName, and add it toSeedson the other nodes if you use seeds. The leader admits it and moves some partitions to it. - Removing a node: stop it. After
MemberRemovalGraceSecondsit 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
Do
Do
- 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
NodeNameand persistent storage forDataDirand the durability data. - Deploy LoT through any one node and let the cluster place it; pin an exclusive Connector to a node with
CLUSTER_NODEonly 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
Don't
- Don’t run 2 nodes: losing either one stops QoS 1/2 publishing, because 1 of 2 is not a majority.
- Don’t set
ReplicationFactorto1when you need messages to survive a node failure. - Don’t change
PartitionCounton 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
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.

