---
title: "Quality of Service (QoS)"
description: "What the QoS 0, 1 and 2 levels govern in NoLag, what the WebSocket connection acknowledges, and which delivery guarantees the platform does not make."
---

# 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](/docs/concepts/replay).

## 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.

```typescript [TypeScript]
const room = client.setApp(APP_SLUG).setRoom('factory-floor')

// Fire and forget on the broker hop
room.emit('temperature', { temperature: 72.5 }, { qos: 0 })
```
```python [Python]
from nolag import EmitOptions, QoS

room = client.set_app(APP_SLUG).set_room('factory-floor')

# Fire and forget on the broker hop
await room.emit('temperature', {'temperature': 72.5}, EmitOptions(qos=QoS.AT_MOST_ONCE))
```
```go [Go]
room := client.SetApp(appSlug).SetRoom("factory-floor")

// Fire and forget on the broker hop
room.Emit("temperature", map[string]any{"temperature": 72.5}, nolag.EmitOptions{QoS: nolag.QoSLevel(nolag.QoSAtMostOnce)})
```

**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.

```typescript [TypeScript]
// Acknowledged on the broker hop (the default, shown explicitly)
room.emit('notifications', { type: 'alert', message: 'You have a new message' }, { qos: 1 })
```
```python [Python]
# Acknowledged on the broker hop (the default, shown explicitly)
await room.emit('notifications', {'type': 'alert', 'message': 'You have a new message'}, EmitOptions(qos=QoS.AT_LEAST_ONCE))
```
```go [Go]
// Acknowledged on the broker hop (the default, shown explicitly)
room.Emit("notifications", map[string]any{"type": "alert", "message": "You have a new message"}, nolag.EmitOptions{QoS: nolag.QoSLevel(nolag.QoSAtLeastOnce)})
```

**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.

```typescript [TypeScript]
// Deduplicated on the broker hop only
room.emit('orders', { orderId: '12345', status: 'completed', amount: 99.99 }, { qos: 2 })
```
```python [Python]
# Deduplicated on the broker hop only
await room.emit('orders', {'orderId': '12345', 'status': 'completed', 'amount': 99.99}, EmitOptions(qos=QoS.EXACTLY_ONCE))
```
```go [Go]
// Deduplicated on the broker hop only
room.Emit("orders", map[string]any{"orderId": "12345", "status": "completed", "amount": 99.99}, nolag.EmitOptions{QoS: nolag.QoSLevel(nolag.QoSExactlyOnce)})
```

**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.

```typescript [TypeScript]
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')
})
```
```python [Python]
def on_published(err):
    if err:
        # Not acknowledged: unknown room, no publish permission, or a timeout.
        print('publish failed:', err)
    else:
        print('accepted by the broker')

await room.emit('orders', {'orderId': '12345', 'status': 'completed'}, EmitOptions(qos=QoS.AT_LEAST_ONCE), on_published)
```
```go [Go]
// Emit returns an error only when the frame could not be sent locally.
// Broker refusals (unknown room, no permission) arrive on OnError.
client.OnError(func(err *nolag.ServerError) {
    fmt.Println("refused:", err.Name, err.Topic, err.Hint)
})
if err := room.Emit("orders", map[string]any{"orderId": "12345", "status": "completed"}); err != nil {
    fmt.Println("not sent:", err)
}
```

## Choosing the Right QoS

### Decision Guide

- **Can you tolerate lost messages?** &rarr; Use QoS 0
- **Need the broker hop acknowledged?** &rarr; Use QoS 1
- **Want the broker hop to deduplicate as well?** &rarr; Use QoS 2, and still dedupe on the client
- **Not sure?** &rarr; 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:

```typescript [TypeScript]
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))
  }
})
```
```python [Python]
processed_messages = set()

def handle_order(data, meta):
    # Use a unique identifier from your data
    message_id = data['orderId']

    # Skip if already processed
    if message_id in processed_messages:
        return

    # Process the message
    process_order(data)

    # Mark as processed
    processed_messages.add(message_id)

    # Clean up old IDs periodically
    if len(processed_messages) > 10000:
        to_remove = list(processed_messages)[:5000]
        for id in to_remove:
            processed_messages.discard(id)

room.on('orders', handle_order)
```
```go [Go]
processedMessages := make(map[string]bool)
mu := &sync.Mutex{}

room.Subscribe("orders", func(data any, meta nolag.MessageMeta) {
    msg, _ := data.(map[string]any)
    messageId, _ := msg["orderId"].(string)

    mu.Lock()
    defer mu.Unlock()

    // Skip if already processed
    if processedMessages[messageId] {
        return
    }

    // Process the message
    processOrder(msg)

    // Mark as processed
    processedMessages[messageId] = true

    // Clean up old IDs periodically
    if len(processedMessages) > 10000 {
        i := 0
        for id := range processedMessages {
            if i >= 5000 {
                break
            }
            delete(processedMessages, id)
            i++
        }
    }
})
```

## Default QoS

You can set a default QoS level for the connection and override it per message:

```typescript [TypeScript]
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 })
```
```python [Python]
from nolag import NoLag, NoLagOptions, EmitOptions, QoS

# Default QoS for every publish and subscription on this connection
client = NoLag('your_access_token', NoLagOptions(
    qos=QoS.AT_LEAST_ONCE
))

await client.connect()
room = client.set_app(APP_SLUG).set_room('factory-floor')

# Override per message
await room.emit('orders', data, EmitOptions(qos=QoS.EXACTLY_ONCE))
```
```go [Go]
// Declared on the client options and per publish, but see the note below
client := nolag.New("your_access_token", nolag.Options{
    QoS: nolag.QoSLevel(nolag.QoSAtLeastOnce), // omit to keep the default
})

if err := client.Connect(); err != nil {
    log.Fatal(err)
}
room := client.SetApp(appSlug).SetRoom("factory-floor")

room.Emit("orders", data, nolag.EmitOptions{QoS: nolag.QoSLevel(nolag.QoSExactlyOnce)})
```


## Next Steps

- [Replay and Durable Delivery](/docs/concepts/replay)
- [Learn about Access Control](/docs/concepts/acl)
- [API Reference](/docs/api-reference)
