---
title: "MQTT QoSとは？0・1・2の違いと選び方をmosquitto・Pythonの実測で解説"
url: "https://www.issoh.co.jp/tech/details/3611/"
published: 2024-09-18
updated: 2026-10-20
categories: ["プロトコル"]
publisher: "株式会社一創"
---

# MQTT QoSとは？0・1・2の違いと選び方をmosquitto・Pythonの実測で解説

MQTTのQoS（Quality of Service）は、メッセージがどこまで確実に届くかを決める「到達保証のレベル」です。0・1・2の3段階があり、番号が上がるほど確実になる一方で、確認のやり取りが増えて遅くなります。この記事では各レベルの送達フローと違い、mosquittoとPythonで実際に送受信した結果、再送と重複の扱い、ブローカーのキュー設定、用途ごとの選び方までを扱います。MQTTそのものの全体像やブローカーの役割は[MQTTブローカーとは？仕組み・主要ブローカー比較とMosquittoでの構築手順](/tech/details/3894/)で解説しています。

QoSの定義は、標準化団体OASISが公開している[MQTT Version 5.0の仕様書](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html)の4.3節（Quality of Service levels and protocol flows）にあります。3.1.1系の挙動は[MQTT Version 3.1.1の仕様書](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)が根拠です。本文の実行結果は、2026年10月20日にmosquitto 2.1.2とpaho-mqtt 2.1.0（Python 3.9.6）を同じPC上で動かして取ったものです。

## まとめ：MQTT QoSの結論

- **QoS 0（最大1回）**：確認なしで送りっぱなし。取りこぼしを許せる高頻度センサー向き。
- **QoS 1（少なくとも1回）**：PUBACKで確認し、届かなければ再送する。重複はありうる。IoTの制御・通知はここが基準。
- **QoS 2（正確に1回）**：4つのパケットで、MQTTの区間での損失と重複配信を防ぐ。受け取ったあとの業務処理が1回で済むことまでは保証しない。
- **処理時間は1万件でQoS 0が約4.7秒、QoS 1が約7.3秒、QoS 2が約10.0秒**（同一PC内の実測）。QoS 2はQoS 0の約2.1倍かかった。
- **実効QoSはpublishのQoSと、購読でブローカーが認めたQoSの小さい方**で決まる。購読側を2にしても、配信元が0なら0で届く。
- **QoS 1でもブローカーのキュー上限を超えると購読者向けは捨てられる**。mosquittoの既定（1000件）のまま1万件を一気に送ると、受信は7,097〜7,383件だった（送信側は全件PUBACKを受信）。
- **切断中のメッセージをキューで受け取るには**、QoS 1以上と永続セッションの両方が要る（mosquittoの既定）。最新値1件だけならリテインメッセージで足りる。
- **AWS IoT CoreはQoS 2に対応していない**。使うブローカーの上限を先に確かめる。

迷ったらQoS 1を基準にし、取りこぼしを許せるならQoS 0へ下げ、MQTTの区間で重複配信そのものを避けたいときにQoS 2を検討します。二重処理を防ぐ本体は受信側の冪等化で、これはQoS 2を選んでも残ります。QoSを上げても防げない取りこぼし（キュー上限・処理中の障害）は別に手当てします。以降で根拠とコードを示します。

## MQTT QoSとは：メッセージ到達保証の水準を0・1・2で選ぶ仕組み

QoSは、送信したメッセージが受信側に届くことをどこまで保証するかを表すMQTT固有の仕組みです。番号が大きいほど確実に届きますが、そのぶん送達確認のパケット往復が増え、遅延・通信量・端末の消費電力が増えます。信頼性とコストのトレードオフを一段階ずつ選べる点がMQTTの特徴です。

混同しやすい用語と切り分けておきます。ネットワーク機器の帯域制御を指す**ネットワークQoS**（DiffServ等の優先制御）とは無関係で、MQTTのQoSはアプリケーション層の到達保証です。利用者の体感品質を表す**QoE（Quality of Experience）**とも別概念です。

見落とされやすいのが、QoSが**「配信元→ブローカー」と「ブローカー→購読者」の2区間それぞれに独立して適用される**ことです。エンドツーエンドで1つのQoSが貫くわけではありません。仕様書も送達プロトコルを送信側と受信側の対称なやり取りとして定義しており、クライアントとサーバーのどちらも送信側になります。この性質が後述する実効QoSとキュー上限の話につながります。

## QoS 0・1・2の送達フローと確認パケットの比較

### QoS 0：At most once（最大1回）の確認応答なしの配信

配信元は`PUBLISH`を一度送るだけで、確認応答を待ちません。ネットワークが切れればメッセージは失われ、再送もされません。往復がないため最速で、毎秒更新される温度センサーのように「最新値だけ届けばよく、1つ2つ落ちても支障がない」データに向きます。

### QoS 1：At least once（少なくとも1回）のPUBACK確認と重複配信

配信元は`PUBLISH`を送り、受信側が`PUBACK`を返します。PUBACKが返らないまま接続が切れると、再接続後に配信元が同じメッセージを送り直すため、応答だけが失われた場合は二重に届きます。再送分には`DUP`フラグが立ちます。設備の状態通知やコマンド送信など、取りこぼしは困るが重複を吸収できる用途で使います。

### QoS 2：Exactly once（正確に1回）の4パケット・2往復による配信

`PUBLISH`→`PUBREC`→`PUBREL`→`PUBCOMP`の4つのパケットで、損失も重複も起こさずちょうど1回の配信を保証します。仕様書は「損失も重複も許されない場面のためのレベルで、オーバーヘッドが増える」と書いています。ただし保証するのはクライアントとブローカーの間の配信までです。課金・在庫の増減のように二重処理が実害になる処理では、QoS 2を選んでも受信側の重複対策は省けません。

| レベル   | 意味      | 確認のパケット               | 損失   | 重複   |
| ----- | ------- | --------------------- | ---- | ---- |
| QoS 0 | 最大1回    | なし                    | ありうる | なし   |
| QoS 1 | 少なくとも1回 | PUBACK                | なし   | ありうる |
| QoS 2 | 正確に1回   | PUBREC・PUBREL・PUBCOMP | なし   | なし   |

表の「損失なし」は、セッションが保たれ、ブローカーのキューにも収まっている前提での話です。クリーンセッションで接続し直すと未確認のメッセージは捨てられ、キューが溢れたときもQoS 1・2のメッセージが落ちます。後者は後半の実測の章で扱います。

## mosquittoでQoS 0・1・2を送受信して往復を確かめる手順

mosquittoのコマンドラインクライアントでは、[mosquitto\_pubのマニュアル](https://mosquitto.org/man/mosquitto%5Fpub-1.html)にあるとおり`-q`でQoSを指定します。指定しなければQoS 0です。`-d`を付けると、送受信した制御パケットがデバッグ出力に出ます。

```
# 端末1：QoS 2で購読（-v でトピック名、-d で制御パケットを表示）
mosquitto_sub -h localhost -t 'sensors/#' -q 2 -v -d

# 端末2：QoS 0・1・2で1通ずつ送る
mosquitto_pub -h localhost -t sensors/temp -q 0 -m "20.5" -d
mosquitto_pub -h localhost -t sensors/temp -q 1 -m "21.5" -d
mosquitto_pub -h localhost -t sensors/temp -q 2 -m "22.5" -d
```

送信側（端末2）のデバッグ出力は次のとおりでした（接続と切断の行は一部省略）。QoS 0はPUBLISHを送ってすぐ切断し、QoS 1はPUBACKを1つ、QoS 2はPUBREC・PUBREL・PUBCOMPの3つを余分にやり取りしています。

```
Client null sending PUBLISH (d0, q0, r0, m1, 'sensors/temp', ... (4 bytes))
Client null sending DISCONNECT

Client null sending PUBLISH (d0, q1, r0, m1, 'sensors/temp', ... (4 bytes))
Client null received PUBACK (Mid: 1, RC:0)

Client null sending PUBLISH (d0, q2, r0, m1, 'sensors/temp', ... (4 bytes))
Client null received PUBREC (Mid: 1)
Client null sending PUBREL (m1)
Client null received PUBCOMP (Mid: 1, RC:0)
```

購読側（端末1）では、QoS 0で送った1通目が`q0`のまま届き、QoS 2で購読していても確認のパケットは出ませんでした。次の章で扱う実効QoSが、この出力でそのまま見えます。

切断中のメッセージが届く条件も同じ手順で確かめられます。[mosquitto\_subのマニュアル](https://mosquitto.org/man/mosquitto%5Fsub-1.html)によると、`-c`はクリーンセッションを無効にして永続クライアントとして接続するオプションで、`-i`で固定したクライアントIDとセットで使います。

```
# 端末1：永続セッションでQoS 1購読（クライアントIDを固定）
mosquitto_sub -h localhost -t 'factory/line1/#' -q 1 -c -i alarm-worker-1 -v
# 端末1を Ctrl+C で止めてから、端末2でQoS 1とQoS 0を1通ずつ送る
mosquitto_pub -h localhost -t factory/line1/alarm -q 1 -m "qos1-while-offline"
mosquitto_pub -h localhost -t factory/line1/alarm -q 0 -m "qos0-while-offline"
# 端末1を同じコマンドで起動し直す
```

起動し直した端末1に届いたのは`qos1-while-offline`の1通だけで、QoS 0の分は届きませんでした。デバッグ出力では、溜まっていたPUBLISHが購読のやり直し（SUBACK）より先に届いています。ブローカーが購読情報をセッションごと保持していたためです。`-c`を外して同じ手順を踏むと、何も届きませんでした。

## publishとsubscribeで異なるQoS：実効QoSは小さい方に下がる仕組み

MQTTでよくある誤解が「購読側でQoS 2にすれば常に正確に1回届く」というものです。仕様書の3.8.4節は、購読者へ送るメッセージのQoSを「元のメッセージのQoSと、サーバーが認めた最大QoSの小さい方にしなければならない」と定めています。低い方へのダウングレードは起きますが、引き上げは起きません。

| publishのQoS | 購読で許可されたQoS | 購読者に届くQoS |
| ----------- | ----------- | --------- |
| 0           | 2           | 0         |
| 2           | 0           | 0         |
| 2           | 1           | 1         |
| 1           | 2           | 1         |
| 2           | 2           | 2         |

2行目は手元でも確かめました。QoS 0で購読し、QoS 2で送ると、購読側には`q0`のPUBLISHが1つ届くだけで、PUBREC以降のやり取りは起きませんでした。確実性を担保するには配信元と購読側の双方で必要なQoSを指定します。設計時は「経路の中で一番低いQoSはどこか」を先に確認してください。

mosquittoには購読側のQoSへ引き上げて配信する`upgrade_outgoing_qos`という設定があります。[mosquitto.confのマニュアル](https://mosquitto.org/man/mosquitto-conf-5.html)は、これを仕様にない非標準の設定と明記しており、既定は無効です。引き上げても配信元からブローカーまでの区間はQoS 0のままなので、その区間の取りこぼしは防げません。

## PythonのpahoでQoS 1の送受信と重複排除を実装するコード例

アプリケーションに組み込む場合は、Eclipse Pahoの[paho-mqtt](https://pypi.org/project/paho-mqtt/)を使うのが一般的です。2026年10月20日時点のPyPIの最新は2.1.0（2024年4月29日公開）です。2.0でコンストラクタにコールバックAPIの版（callback\_api\_version）を指定する引数が加わりました。2.1.0では省略するとVERSION1（1.x系と同じ引数のコールバック）になり、非推奨の警告が出ます。新しく書くコードはVERSION2を明示します。書き換え方は[pahoの移行ガイド](https://eclipse.dev/paho/files/paho.mqtt.python/html/migrations.html)にあります。ループ方式や再接続の作り方は[Python MQTT実装｜paho-mqtt 2系のループ方式と再接続を実装目線で解説](/tech/details/16866/)で扱っています。

次のコードは、QoS 1で購読して業務キー（event\_id）で重複を弾く受信側と、PUBACKを待つ送信側の最小構成です。MQTT 5.0で接続し、クリーンスタートを無効にしてセッション有効期限を1時間にしているので、受信側が一時的に落ちても再接続後に未受信分を受け取れます。送信側は、応答待ちでタイムアウトしたアプリが同じイベントを送り直した場面を再現するために2回送っています。

```
# pip install "paho-mqtt==2.1.0"
import json
import threading
import time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

BROKER = "localhost"
TOPIC = "factory/line1/alarm"
processed = set()  # 処理済みの業務キー（本番はDBの一意制約で持つ）
subscribed = threading.Event()


def on_connect(client, userdata, flags, reason_code, properties):
    print("connected:", reason_code, "session_present:", flags.session_present)
    client.subscribe(TOPIC, qos=1)


def on_subscribe(client, userdata, mid, reason_code_list, properties):
    # SUBACKでブローカーが認めたQoS（granted QoS）を確認する
    print("granted qos:", [rc.value for rc in reason_code_list])
    subscribed.set()


def on_message(client, userdata, msg):
    event = json.loads(msg.payload)
    if event["event_id"] in processed:
        print("duplicate skipped:", event["event_id"], "dup:", msg.dup)
        return
    processed.add(event["event_id"])
    print("handled:", event, "qos:", msg.qos, "dup:", msg.dup)


# 受信側：MQTT 5.0の永続セッションでQoS 1購読
props = Properties(PacketTypes.CONNECT)
props.SessionExpiryInterval = 3600  # 切断後1時間はセッションを保持させる
sub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                  client_id="alarm-worker-1", protocol=mqtt.MQTTv5)
sub.on_connect = on_connect
sub.on_subscribe = on_subscribe
sub.on_message = on_message
sub.connect(BROKER, 1883, keepalive=60, clean_start=False, properties=props)
sub.loop_start()
subscribed.wait(timeout=5)  # SUBACKを受け取るまで送信しない

# 送信側：QoS 1で送り、PUBACKを受け取るまで待つ
pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
pub.connect(BROKER, 1883)
pub.loop_start()
body = json.dumps({"event_id": "A-1001", "level": "high"})
for attempt in range(2):  # 応答待ちでタイムアウトしたアプリが送り直した想定
    info = pub.publish(TOPIC, body, qos=1)
    info.wait_for_publish(timeout=5)
    print("acked:", info.is_published())

time.sleep(1)
pub.loop_stop()
pub.disconnect()
sub.loop_stop()
sub.disconnect()
```

実行すると、2通目は`duplicate skipped: A-1001 dup: False`として捨てられました。ここで`dup`がFalseのままなのが要点です。DUPフラグが立つのはMQTTのプロトコルが送り直したときだけで、アプリが新しいPUBLISHとして送り直した重複には立ちません。重複判定をDUPフラグに頼らず、本文の業務キーで行う理由がここにあります。

押さえどころは他に2つあります。`wait_for_publish`で待てるのは「ブローカーまで届いたこと」までで、購読者に届いたことは保証されません（後述の実測で、送信側が全件PUBACKを受け取っても購読者には一部しか届かない例を示します）。重複排除の記録は、例ではメモリ上の集合ですが、プロセスが再起動すると消えるため、本番ではデータベースの一意制約や処理済みテーブルで持ちます。受信処理を冪等にする設計は[冪等性とは？読み方・意味からAPI・IaCでの担保方法まで実装者向けに解説](/tech/details/13382/)で詳しく扱っています。

## QoS 1・2の再送とDUPフラグ：PUBACK未応答で切断したときの再送の検証

QoS 1・2のメッセージにはパケット識別子が付き、確認応答が来るまで送信側が保持します。MQTT 5.0の仕様書4.4節は、クリーンスタートを0にして再接続し、セッションが残っていた場合に限り、未確認のPUBLISHとPUBRELを元のパケット識別子のまま再送すると定めています。それ以外のタイミングで再送してはならない、とも書かれています。接続中にタイマーで再送する実装を前提にした設計は、5.0では成り立ちません。

この動きは、paho-mqttの`manual_ack=True`でPUBACKの送信を止めると観察できます。

```
# QoS 1でPUBACKを返さずに切断し、再接続後の再送を観察する
import time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

TOPIC = "factory/line1/dup"


def connect_worker(send_ack):
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="dup-worker",
                    protocol=mqtt.MQTTv5, manual_ack=True)  # PUBACKを自分で送る

    def on_connect(client, userdata, flags, reason_code, properties):
        print("session_present:", flags.session_present)
        client.subscribe(TOPIC, qos=1)

    def on_message(client, userdata, msg):
        print("mid=%d dup=%s payload=%s" % (msg.mid, msg.dup, msg.payload.decode()))
        if send_ack:
            client.ack(msg.mid, msg.qos)

    c.on_connect = on_connect
    c.on_message = on_message
    props = Properties(PacketTypes.CONNECT)
    props.SessionExpiryInterval = 3600
    c.connect("localhost", 1883, clean_start=False, properties=props)
    c.loop_start()
    time.sleep(1)
    return c


w = connect_worker(send_ack=False)      # 1回目：受け取るがPUBACKを返さない
pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
pub.connect("localhost", 1883)
pub.loop_start()
pub.publish(TOPIC, "A-1001", qos=1).wait_for_publish(5)
time.sleep(1)
w.disconnect()
w.loop_stop()

w = connect_worker(send_ack=True)       # 2回目：同じクライアントIDで再接続
time.sleep(1)
w.disconnect()
pub.disconnect()
```

```
session_present: False
mid=1 dup=False payload=A-1001
session_present: True
mid=1 dup=True payload=A-1001
```

再接続後、同じ`mid=1`のメッセージが`dup=True`で届き直しました。受信側が処理を終えてからPUBACKを返す前に落ちると、このように同じ内容を2回受け取ります。パケット識別子は再接続をまたいで使い回されるため、重複判定のキーには向きません。前章のコードのように業務上一意なIDで判定してください。

QoS 2はブローカーがPUBREC・PUBRELの状態を識別子単位で持ち、二重配信を抑止するため、アプリ側で重複対策をしなくてもプロトコル上は1回になります。ただし保証の範囲は送信側と受信側のクライアントライブラリの間までで、受け取ったあとの業務処理の途中でプロセスが落ちた場合までは守れません。

順序については、仕様書4.6節が、非共有購読ではすべてのトピックを「Ordered Topic」として扱い、同じクライアントから同じQoSで送られたメッセージを受け取った順に購読者へ送るよう定めています。QoSの違うメッセージどうしは対象外です。同節の注記によると、再送が絡むと1,2,3,4の順に送ったメッセージが1,2,3,2,3,4のように重複を挟んで届くことがあります。mosquitto.confのマニュアルには、`max_inflight_messages`を1にすると順序どおりの配信が保証されると書かれていますが、1件ずつ確認を待つぶん遅くなります。

## QoS別の処理時間とキュー上限：1万件を送った実測

QoSを上げるとどれだけ遅くなるのかを、同じPC上のmosquitto 2.1.2とpaho-mqtt 2.1.0で測りました。32バイトのメッセージ1万件を1つの送信クライアントから一気にpublishし、同じQoSで購読したクライアントが全件を受け取るまでの時間を、0.05秒間隔で受信件数を確かめる方法で、各QoSで3回ずつ計測しています（macOS 26.6.2・Intel Core i9-9880H、同じPC内のループバック通信）。

| QoS | 1万件の受信完了まで（3回）      | 平均     | QoS 0との比 |
| --- | ------------------- | ------ | -------- |
| 0   | 4.71秒・4.70秒・4.59秒   | 約4.7秒  | 1.0倍     |
| 1   | 7.39秒・7.05秒・7.43秒   | 約7.3秒  | 約1.6倍    |
| 2   | 9.99秒・10.05秒・10.00秒 | 約10.0秒 | 約2.1倍    |

同じPC内でも、QoS 2はQoS 0の約2.1倍の時間がかかりました。回線の往復遅延がある環境での比率はこの計測では確かめていないため、実際の回線と端末で測り直してください。

この表は、ブローカーの`max_queued_messages`を20000に上げて取ったものです。**mosquittoの既定（1000）のままだと、QoS 1の購読者が受け取れたのは1万件中7,383件・7,106件・7,097件でした**。3回とも送信側は1万件すべてについてブローカーからPUBACKを受け取っており、ブローカーのログには`Outgoing messages are being dropped for client`が出ていました。ログと設定値から見て、購読者への送信待ちが「確認待ち20件＋キュー1000件」を超えた分が捨てられています。

QoS 1は区間ごとの到達保証であり、ブローカーが受け取ったメッセージを購読者へ必ず届けるところまでは約束しません。同じ条件のQoS 2では3回とも取りこぼしがありませんでした。送信側自身が確認待ちで遅くなり、購読者の処理が追いついたためと考えられます（キュー長の推移は記録していません）。QoS 2にすれば安全という意味ではなく、流量が購読者の処理能力を上回るとQoSに関係なく溢れます。

mosquittoのキューに関わる設定は次の3つです。値と説明は[mosquitto.confのマニュアル](https://mosquitto.org/man/mosquitto-conf-5.html)の記載です。

```
# /etc/mosquitto/conf.d/qos.conf
# 同時に応答待ちにできるQoS 1・2の送信数（既定20、1にすると順序保証）
max_inflight_messages 20
# 応答待ちの分を超えてクライアントごとに溜めるQoS 1・2の件数（既定1000、0は無制限）
max_queued_messages 1000
# 再接続しない永続クライアントのセッションを7日で破棄する（非標準の設定）
persistent_client_expiration 7d
```

キュー長は「購読者が処理を止めうる時間（切断を含む）×その間の流入件数」で見積もります。突発的に大量のメッセージが来るなら上限を上げ、それでも溢れるならブローカーのログを監視して検知し、購読者を増やす（MQTT 5.0の共有サブスクリプション）か、ゲートウェイ側で一時保存して再送する構成にします。MQTT 3.1.1の永続セッションや、有効期限を無期限にしたMQTT 5.0のセッションでは、`persistent_client_expiration`を設定しないと撤去した端末のセッションがブローカーに残り続けます。MQTT 5.0では受信側が接続時にReceive Maximumで同時に処理できるQoS 1・2の件数を宣言でき、省略時の値は65,535です。仕様書には、QoS 0の送信を制限する仕組みは無いとも書かれています。

## QoSレベルの選定基準：欠落の影響と重複への対処

レベルは高いほど良いわけではありません。「失われて困るか」「二重に処理されて困るか」の2軸で決めるのが実務的です。

| データの例         | 欠落の影響 | 重複の影響  | 推奨QoS          |
| ------------- | ----- | ------ | -------------- |
| 温度・振動の定期計測    | 小さい   | 小さい    | 0              |
| 設備の異常アラート     | 大きい   | 冪等化で吸収 | 1              |
| 遠隔の停止・再起動コマンド | 大きい   | 冪等化で吸収 | 1              |
| 課金・ポイント加算     | 大きい   | 実害あり   | 1または2＋業務側の重複対策 |

受信側を冪等（同じメッセージを二度処理しても結果が変わらない設計）にしてQoS 1で運用するほうが、QoS 2の重い往復を避けられて有利な場面が多くあります。前章の実測では、QoS 1はQoS 2より3割近く短い時間で終わりました。なんとなく最上位を選ぶと、通信量と遅延を無駄に増やします。

ゲートウェイでセンサー群を束ねてクラウドへ送る構成なら、センサーとゲートウェイの間はQoS 0、ゲートウェイからクラウドへの集約送信はQoS 1、と区間ごとに変える設計もよく採られます。ゲートウェイを挟むか直結するかの判断は[IoTゲートウェイとは？プロトコル変換の役割とクラウド直結との使い分け](/column/details/16902/)で整理しています。

## オフライン配信とQoS：永続セッションとリテインメッセージの使い分け

購読者が一時的に切断していても後からメッセージを受け取りたい場合、QoSだけでは足りません。QoS 1以上に加えて、永続セッション（MQTT 3.1.1では接続時にclean sessionを無効化、MQTT 5.0ではSession Expiry Intervalを設定）が必要です。この組み合わせで、ブローカーは切断中の購読者宛メッセージをキューに保持し、同じクライアントIDでセッションを再開したときに配信します（MQTT 5.0では再接続時もClean Startを0にします）。mosquittoはQoS 0のメッセージを既定では溜めません（`queue_qos0_messages`を有効にすると溜めます）。溜められる件数は前章の`max_queued_messages`に従います。

切断中の端末に「最後の値」だけを届けたいなら、キューではなくリテインメッセージ（保持メッセージ）を使います。リテインメッセージは購読が成立した時点で届くため、端末側は接続のたびにSUBSCRIBEを送る作りにします。仕様書は、リテインメッセージはセッションの状態に含まれず、セッションが終わっても消えないと定めています。

| 届けたいもの     | 使う仕組み     | 必要な設定           |
| ---------- | --------- | --------------- |
| 切断中の全メッセージ | 永続セッション   | QoS 1以上＋セッション保持 |
| 最新の値1件だけ   | リテインメッセージ | 送信時にretainを付ける  |
| どちらも不要     | クリーンセッション | QoS 0でもよい       |

## AWS IoT Coreなどマネージドブローカーの対応QoSと上限

MQTT 5.0では、サーバーが対応する最大QoSを接続時の応答（CONNACK）の`Maximum QoS`で通知します。仕様書3.2.2.3.4節によると、値は0か1で、省略時は2として扱われます。クライアントはそれを超えるQoSでPUBLISHしてはならず、超えた場合はサーバーが理由コード0x9B（QoS not supported）で切断します。mosquittoでもリスナーごとに`max_qos`で上限を下げられます。

クラウド側でMQTTを終端する構成では、[AWS IoT Coreとは？機能・料金・MQTT接続方法をまとめて解説](/tech/details/2991/)のようにマネージドブローカーが対応QoSを決めています。[AWS IoT Coreの開発者ガイド（MQTT）](https://docs.aws.amazon.com/iot/latest/developerguide/mqtt.html)には、Maximum QoSの既定が1で、QoS 2はサポートしないと書かれています。QoS 2を前提にしたシステムはそのまま移せないため、採用前に対応レベルを確認してください。切断中の端末へのメッセージ保持にも独自の条件と上限があり、詳細は[AWS IoT Coreのメッセージキューイングとは？共有サブスクリプションでQoS1を保持する条件と上限](/tech/details/8039/)で扱っています。

自前でブローカーを立てる場合の選び方は[EMQXとは？OSS版の無償範囲とMosquittoとの違い](/tech/details/1498/)が参考になります。ブローカーを選ぶ段階で「QoS 2が要るか」「永続セッションを何台分・何件まで保持するか」を要件として書き出しておくと、後から乗り換える手戻りを防げます。

## MQTTとHTTPの違い：QoSの到達保証があるかで変わる設計の考え方

HTTPはクライアントが要求し都度応答を得るリクエスト/レスポンス型で、届いたかどうかはアプリケーションが判断します。到達保証をレベルで選ぶ仕組みは標準にありません。対してMQTTは配信/購読（Pub/Sub）型で、常時接続の上にQoSという到達保証を持ちます。QoS以外の仕様（制御パケット・キープアライブ・MQTT 5.0の共有購読）を含めた全体像は[MQTTとは？Pub/Subの仕組み・QoS・MQTT 5.0の実装からHTTPとの使い分けまで実装者向けに解説](/tech/details/15819/)で解説しています。

HTTPで同じ確実さを得ようとすると、送信側のリトライ、受信側の重複排除、未送信分の保存をすべてアプリケーションで作ることになります。MQTTのQoS 1と永続セッションは、このうち再送と保存をプロトコルとブローカーに任せられる点が利点です。重複排除はどちらを選んでも受信側に残ります。

## QoS 2を採用する条件と見送る場面：IoT受託開発での判断基準

ここまでの内容を踏まえ、QoS 2を採用するかどうかは次の条件で判断します。

- **採用する**：ブローカーとの区間で重複配信そのものを減らしたく、ブローカーがQoS 2に対応し、送信頻度が低い場合。外部の決済APIを叩く、物理的な動作を1回だけ行わせるといった処理では、QoS 2に加えて業務キーによる重複対策が要ります。
- **見送ってQoS 1＋冪等化にする**：受信側でデータベースの一意制約や処理済み記録を持てる場合。ほとんどの業務データはこちらに入ります。
- **見送ってQoS 0にする**：高頻度で上書きされる計測値で、欠けた分を次の値が補う場合。

QoS 2が「正確に1回」を保証するのはクライアントとブローカーの区間ごとです。購読者が受け取ったあと業務処理の途中でプロセスが落ちれば、その処理は失われますし、実測で見たとおりブローカーのキューが溢れれば届きません。端から端まで1回だけ処理することを本当に保証したいなら、受信側の冪等化と処理済み記録、キュー溢れの監視が必要です。QoS 2を選ぶ前に「QoS 1と冪等化で足りないか」を先に検討してください。

センサーや設備からデータを集めるシステムを新たに作る場合、QoSの選定はブローカー選び・ゲートウェイの有無・クラウド側の受信処理と一体で決まります。一創では、センサー連携からクラウドでのデータ蓄積・分析までを[AI/IoTソリューション](/service/ai/iot/)として受託しており、QoSや永続セッションの設計を含めた構成から相談を受けています。

## よくある質問

### MQTTのQoSとは何ですか？

メッセージの到達をどこまで保証するかを表すレベルで、0（最大1回）・1（少なくとも1回）・2（正確に1回）の3段階があります。番号が大きいほど確実に届く反面、確認の往復が増えて遅くなります。同じPC内の実測では、1万件の処理時間がQoS 0で約4.7秒、QoS 2で約10.0秒でした。

### QoS 1とQoS 2の違いは何ですか？

QoS 1はPUBACKの1往復で確認し、応答が失われると再送するため重複がありえます。QoS 2はPUBREC・PUBREL・PUBCOMPを加えた2往復で、重複も防ぎます。そのぶんQoS 2は遅く、実測ではQoS 1の約1.4倍の時間がかかりました。受信側を冪等にできるならQoS 1で足ります。

### QoSはどのレベルを使えばよいですか？

取りこぼしを許せる高頻度データはQoS 0、確実に届けたいが重複を吸収できるならQoS 1、MQTTの区間で重複配信を避けたいならQoS 2が目安です。決済のように二重処理が実害になる処理は、QoSに関係なく業務キーでの重複対策を組み合わせます。多くのIoT用途はQoS 1を基準に、受信側を冪等化して運用します。AWS IoT CoreのようにQoS 2に対応しないブローカーもあるため、使う予定のブローカーの対応範囲も先に確認してください。

### MQTT QoS 1では重複が起きますか？

起こりえます。PUBACKが届かないまま接続が切れると、再接続後に同じメッセージがDUPフラグ付きで再送されます。アプリが自分で送り直した場合はDUPフラグが立たないため、重複はメッセージ本文の業務キーで判定し、受信処理を冪等にします。

### MQTTにQoS 3はありますか？

ありません。QoSは0・1・2の3段階だけです。MQTT 5.0の仕様書は、PUBLISHパケットのQoSの2ビットを両方1にする（値3にする）ことを禁じており、受け取った側はMalformed Packet（不正なパケット）として扱い、接続を閉じます（理由コードは0x81）。

## 関連記事

- [MQTTとは？Pub/Subの仕組み・QoS・MQTT 5.0の実装からHTTPとの使い分けまで実装者向けに解説](/tech/details/15819/)
- [MQTTブローカーとは？仕組み・主要ブローカー比較とMosquittoでの構築手順【2.1対応】](/tech/details/3894/)
- [Python MQTT実装｜paho-mqtt 2系のループ方式と再接続を実装目線で解説](/tech/details/16866/)
- [EMQXとは？OSS版の無償範囲とMosquittoとの違い【6.3対応】](/tech/details/1498/)
- [AWS IoT Coreのメッセージキューイングとは？共有サブスクリプションでQoS1を保持する条件と上限](/tech/details/8039/)

---

出典: [MQTT QoSとは？0・1・2の違いと選び方をmosquitto・Pythonの実測で解説](<https://www.issoh.co.jp/tech/details/3611/>)（株式会社一創）
