Quality of Service (QoS)
QoS is the MQTT delivery level NoLag applies on the broker hop of a message's journey. Choose it per message or per connection, and know exactly what it does and does not cover.
What QoS Governs
A message travels two legs: from your client to the NoLag gateway over WebSocket, and from the gateway across the internal MQTT broker to the gateways of every subscriber. The QoS level you set (0, 1 or 2, default 1) is validated and passed to that internal broker hop. Anything outside 0 to 2 is treated as 1.
The WebSocket leg has its own, simpler contract:
- A publish can be acknowledged. On protocol v2 (js-sdk 1.6+, python-sdk 2.5+, go-sdk v0.5.0) the SDK attaches a
msgRefwhen you pass a callback, and the broker answers with apublishedframe once it has accepted the message. That acknowledgement is what the emit callback resolves on. - Nothing is resent. If the acknowledgement does not arrive within 10 seconds the callback receives an error, and it is up to you to publish again.
- There is no exactly-once guarantee end to end. Publishers never receive their own messages, and a subscriber may see a duplicate if a message is published twice, so use an idempotency key in your payload for anything that must not be applied twice.
If your design needs durable catch-up for a worker group, that is a separate feature; see Replay and Durable Delivery.
QoS Levels
| Level | Name | Broker hop behaviour | Use Case |
|---|---|---|---|
0 | At-most-once | Forwarded once, no broker acknowledgement; may be dropped under pressure | Telemetry, cursors, typing |
1 | At-least-once | Acknowledged between broker nodes; may be forwarded more than once | Notifications, chat messages (default) |
2 | Deduplicated on the broker hop | The broker hop runs the MQTT level 2 handshake; the WebSocket leg does not, so this is not an end-to-end guarantee | Where a duplicate is costly and you also dedupe on the client |
QoS 0: At-most-once
The internal hop forwards the message once with no acknowledgement. The cheapest option, and the right one when the next update supersedes this one anyway.
const room = client.setApp(APP_SLUG).setRoom('factory-floor')
// Fire and forget on the broker hop
room.emit('temperature', { temperature: 72.5 }, { qos: 0 })Best for:
- Sensor data that updates frequently
- Live location updates
- Typing indicators
- Mouse cursor positions
QoS 1: At-least-once
The internal hop acknowledges the message between broker nodes and may forward it more than once. This is the default, and the right choice for most application traffic.
// Acknowledged on the broker hop (the default, shown explicitly)
room.emit('notifications', { type: 'alert', message: 'You have a new message' }, { qos: 1 })Best for:
- Chat messages
- Notifications
- Event broadcasts
- Most real-time applications
QoS 2: Deduplicated on the broker hop
The internal hop runs the MQTT level 2 four-step handshake, so the broker does not forward a duplicate between its own nodes. It is the slowest level, and it does not extend to the WebSocket leg: the gateway still delivers what the broker hands it, and a client that publishes twice still produces two messages. Treat QoS 2 as "fewer duplicates", not "no duplicates", and keep the idempotency check below.
// Deduplicated on the broker hop only
room.emit('orders', { orderId: '12345', status: 'completed', amount: 99.99 }, { qos: 2 })Best for:
- Order and payment status events, together with an idempotency key
- Critical state changes that are also deduplicated by the consumer
- Inventory updates where a duplicate would be expensive to reverse
Confirming a Publish
Pass a callback to emit to learn whether the broker accepted the message. On protocol v2 the callback resolves on the broker's published acknowledgement; if none arrives within 10 seconds it receives an error. The SDK never resends on its own.
room.emit('orders', { orderId: '12345', status: 'completed' }, { qos: 1 }, (err) => {
if (err) {
// Not acknowledged: unknown room, no publish permission, or a timeout.
// Decide whether to publish again; the SDK will not.
console.error('publish failed:', err.message)
return
}
console.log('accepted by the broker')
})Choosing the Right QoS
Decision Guide
- Can you tolerate lost messages? → Use QoS 0
- Need the broker hop acknowledged? → Use QoS 1
- Want the broker hop to deduplicate as well? → Use QoS 2, and still dedupe on the client
- Not sure? → Start with QoS 1 (the default)
Handling Duplicates
Whatever the level, implement idempotency on the consumer. Key it on an identifier from your own payload, which survives a publisher retry as well as a broker duplicate:
const processedMessages = new Set<string>()
room.on('orders', (data: { orderId: string }, meta) => {
// Use a unique identifier from your data
const messageId = data.orderId
// Skip if already processed
if (processedMessages.has(messageId)) {
return
}
// Process the message
processOrder(data)
// Mark as processed
processedMessages.add(messageId)
// Clean up old IDs periodically
if (processedMessages.size > 10000) {
const entries = Array.from(processedMessages)
entries.slice(0, 5000).forEach(id => processedMessages.delete(id))
}
})Default QoS
You can set a default QoS level for the connection and override it per message:
import { NoLag } from '@nolag/js-sdk'
// Default QoS for every publish and subscription on this connection
const client = NoLag('your_access_token', {
qos: 1
})
await client.connect()
const room = client.setApp(APP_SLUG).setRoom('factory-floor')
// Override per message
room.emit('orders', data, { qos: 2 })