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 msgRef when you pass a callback, and the broker answers with a published frame 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

LevelNameBroker hop behaviourUse Case
0At-most-onceForwarded once, no broker acknowledgement; may be dropped under pressureTelemetry, cursors, typing
1At-least-onceAcknowledged between broker nodes; may be forwarded more than onceNotifications, chat messages (default)
2Deduplicated on the broker hopThe broker hop runs the MQTT level 2 handshake; the WebSocket leg does not, so this is not an end-to-end guaranteeWhere 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 })

Next Steps