Fundamentals

MQTT Shared Subscriptions: Load Balancing with $share

Learn MQTT shared subscriptions: the $share/group/filter syntax, how messages are distributed, broker support, real use cases and pitfalls like ordering.

Updated · 7 min read

In normal MQTT, every subscriber whose filter matches a topic receives its own copy of each message. That is perfect for fan-out, but awkward when you want several backend workers to split a stream of messages between them. Shared subscriptions, standardised in MQTT 5, solve this: subscribers join a named group, and the broker delivers each message to only one member of that group.

The $share syntax

A shared subscription is a normal subscribe request with a special topic filter:

Format
$share/<ShareName>/<TopicFilter>
  • $share — a literal prefix that marks the subscription as shared.
  • ShareName — the group name. It must be at least one character and must not contain /, + or #.
  • TopicFilter — an ordinary filter, wildcards included. See MQTT topics and wildcards.

For example, $share/ingest/factory/+/telemetry puts the subscriber in the group ingest for every factory/<line>/telemetry topic. Publishers do nothing special: they still publish to factory/line-3/telemetry and never see the $share prefix. To check which topics a filter matches, paste the part after the group name into the MQTT topic matcher.

How messages are distributed

The delivery rules are easiest to see with an example. Assume these subscriptions exist:

ClientSubscriptionReceives a message published to orders/new?
worker-a$share/billing/orders/#One of the two (the broker chooses)
worker-b$share/billing/orders/#
mailer-1$share/email/orders/newYes — it is the only member of group email
auditorders/#Yes — normal subscriptions are unaffected

So each group gets one copy, and inside a group one member handles it. How the member is chosen is not defined by the spec. Brokers offer strategies such as round robin, random, sticky (keep sending to the same member until it goes away) or hashing on the client ID or topic — EMQX, for instance, lets you configure this. Each member keeps its own subscription QoS, and messages are acknowledged per member as usual.

Broker support

Shared subscriptions are part of the MQTT 5 standard, and a broker signals support through the Shared Subscription Available property in CONNACK. If a broker does not support them, it rejects the subscribe with reason code 0x9E (Shared Subscriptions not supported) — see the MQTT reason codes reference.

  • Mosquitto supports $share since version 1.6, for MQTT 5 and 3.1.1 clients.
  • EMQX supports $share and also an older shortcut, $queue/<filter>, which behaves like a shared subscription in a single default group.
  • HiveMQ supports $share for both protocol versions.

Managed and cloud brokers vary, so check the docs before you rely on it. Our MQTT broker comparison covers the main options.

Example: two workers sharing a stream

Open three terminals. The first two join the same group; the third publishes ten messages:

Terminals 1 and 2 — workers
mosquitto_sub -V mqttv5 -h broker.hivemq.com -q 1 \
  -t '$share/workers/testmqtt/share-demo/jobs' -v
Terminal 3 — publisher
for i in $(seq 1 10); do
  mosquitto_pub -V mqttv5 -h broker.hivemq.com -q 1 \
    -t 'testmqtt/share-demo/jobs' -m "job $i"
done

Each job appears in exactly one worker terminal. Note the single quotes around the filter: in most shells, $share inside double quotes would be expanded as a variable. The same works in code:

MQTT.js worker
import mqtt from 'mqtt';

const client = mqtt.connect('wss://broker.hivemq.com:8884/mqtt', { protocolVersion: 5 });

client.on('connect', () => {
  client.subscribe('$share/workers/testmqtt/share-demo/jobs', { qos: 1 });
});

client.on('message', (topic, payload) => {
  // topic is the real topic, without the $share prefix
  console.log(topic, payload.toString());
});

You can be the publisher from your browser: open the client below and publish to testmqtt/share-demo/jobs while your workers are running.

Publish test jobs in the online client

Use cases

  • Horizontal scaling of backend consumers. Run N instances of an ingestion service and let the broker spread telemetry across them, without a separate message queue.
  • High availability. If one consumer crashes, the remaining members keep receiving messages.
  • Bridging into databases or stream processors. Several writers can share the load of inserting messages into a time-series database or forwarding them to Kafka.
  • Work queues. Commands such as "resize this image" can be handled by whichever worker receives them.

Designing a shared subscription setup

Choose group names per consumer type

Name groups after the job, not the instance: $share/ingest/..., $share/alerts/.... Every instance of the ingest service joins ingest; every instance of the alerting service joins alerts. Each service then sees every message exactly once as a whole, while its instances split the work. If you accidentally give each instance a unique group name, you are back to plain fan-out and every instance processes every message.

Sessions and QoS for workers

Workers usually subscribe with QoS 1 so that a message is redelivered if a worker crashes before acknowledging it. Whether messages are queued for a group while all members are offline depends on the members' sessions: with a persistent session the broker can keep queuing; with clean sessions, messages published while nobody in the group is connected are simply not delivered to that group.

Shared subscriptions vs a message queue

Shared subscriptions give you simple competing consumers inside MQTT. They do not give you features of dedicated queueing systems such as replay from an offset, dead-letter queues or per-partition ordering guarantees. For many IoT backends that trade-off is fine; if you need replay, forward the stream from a shared subscription into a log such as Kafka and process it there.

Pitfalls and limitations

  • No global ordering. MQTT guarantees ordering per topic for a single subscriber, but once messages are spread over several workers, they are processed in parallel. If order matters per device, use a broker strategy that hashes on topic, or partition by topic yourself.
  • No retained messages. Per the MQTT 5 spec, retained messages are not sent when a shared subscription is established. Use a normal subscription for state like retained status topics.
  • No Local is not allowed. Setting the No Local option on a shared subscription is a protocol error in MQTT 5.
  • Unsubscribe with the full filter. To leave the group, unsubscribe from the exact $share/group/filter string, not just the plain filter.
  • ACLs. Brokers differ in whether access rules apply to the $share/... string or to the underlying filter. Test your permissions with the real syntax.
  • Duplicates are still possible. QoS 1 means at-least-once, and redelivery to another member after a disconnect can produce duplicates. Design handlers to be idempotent; see MQTT QoS levels.

Frequently asked questions

What is an MQTT shared subscription?

A shared subscription lets several clients subscribe as a group using a topic filter of the form $share/group/filter. Each matching message is delivered to only one member of the group instead of to every subscriber, which spreads the load across multiple consumers.

Do shared subscriptions work with MQTT 3.1.1?

Shared subscriptions are standardised in MQTT 5. Several brokers, including Mosquitto, EMQX and HiveMQ, also accept the $share syntax from MQTT 3.1.1 clients as an extension, but behaviour is broker-specific, so check your broker documentation.

Why do shared subscribers not receive retained messages?

The MQTT 5 specification says retained messages are not sent to a session when it establishes a new shared subscription. Since a retained message represents current state rather than work to distribute, delivering it to one arbitrary group member would not be meaningful. Use a normal subscription if you need the retained value.