Loading post
Jul 18, 2026

チャットの吹き出しは相手の画面へ直接飛ぶわけではない。多くのサービスでは、送信者の端末、サービスのサーバー、Push通知基盤、受信者の複数端末が協調する。暗号化されたサービスでも、配送そのものには宛先・時刻・端末といったメタデータが必要になる。英語版は blog-how-messaging-works.en.md。

送信ボタンを押すと、アプリはローカルなIDを付けてすぐ吹き出しを描き、裏でサーバーへ送る。サーバーは認証・保存・宛先ごとの配信を行う。相手のアプリが接続中ならリアルタイム回線へ流し、休止中ならAPNsやFCMへ「新着がある」と通知する。相手端末はメッセージを取得・復号・保存し、受領確認や既読を返す。
ここで「送信済み」「サーバー受付済み」「相手端末へ配送済み」「相手が開いた」は別々の事実だ。
通信結果を待ってから表示すると、地下鉄や混雑した回線ではアプリが鈍く感じる。そこで多くのアプリは optimistic UI を使う。
client_message_id を作る一瞬で見えるのは通信が一瞬で完了したからではなく、完了を予測して先に描いたからだ。
アプリを開いている時は、WebSocketのような双方向接続、長時間のHTTP接続、独自の接続方式などでサーバーとつながり続けられる。サーバーは新着を待たずに押し出せる。
しかしスマホOSは、電池とメモリを守るためバックグラウンドアプリを休止する。休止中のアプリが自前の接続を永遠に維持するのは難しい。そこでOSベンダーのPush基盤を使う。
Pushは「荷物本体」より 呼び鈴 と考えると分かりやすい。通知へ本文を含める設計もあるが、サイズ、機密性、失効、重複、配送保証に制約がある。AppleもPush配送を保証していない。重要な状態はサービス側の保存と再同期で回復できるようにする。
モバイル通信では次が普通に起きる。
再送しなければ欠落し、無条件で再送すれば重複する。そこで送信側が作った client_message_id を idempotency key(冪等キー) として使う。
(sender_id, client_message_id) に一意制約
初回:
未登録 → 保存 → server_message_idを返す
再試行:
登録済み → 新しく保存せず、同じ結果を返す
「exactly once(必ず一度だけ)」のように見える体験は、ネットワークが一度だけ運ぶからではない。at-least-onceで再送し、受信側で重複排除することで作られることが多い。
二人がほぼ同時に送ると、端末時計はずれているかもしれず、通信経路も違う。クライアントの時刻だけで並べると順番が入れ替わる。
実用上は、サーバーが会話ごとの単調増加番号を割り当てたり、サーバー受付順を基準にしたりする。
| 値 | 用途 |
|---|---|
client_created_at | ユーザーが入力した頃の表示補助 |
server_received_at | サーバーが受けた監査時刻 |
conversation_seq | 会話内の欠落検出と安定した並び |
server_message_id | 永続的な識別 |
大規模なグループチャットを複数リージョンで処理すると、厳密な全順序は遅延・可用性と衝突する。どの範囲で順序を保証するか——会話単位、送信者単位、あるいはベストエフォート——は製品仕様そのものだ。
画面上は小さなチェックマークでも、裏では状態イベントが流れている。
複数端末があると「どの端末への配送をdeliveryとするか」「スマホで読んだ状態をPCへどう反映するか」も決める必要がある。
E2EEでは、本文を読める鍵を基本的に会話参加者の端末だけが持つ。サーバーは暗号文を配送する。
E2EEがあってもサーバーが何も知らないとは限らない。配送には次の metadata が残り得る。
さらに難しいのは鍵のライフサイクルだ。新端末の追加、紛失端末の失効、グループから外れた人が将来のメッセージを読めないこと、鍵が漏れた後も過去ログを守ること、端末間バックアップを設計しなければならない。
両者は代替ではない。E2EEの暗号文もTLSで運び、宛先などの周辺情報を通信途中から守る。
グループへ投稿した時、書き込み時に各受信者の受信箱へ展開すれば読み取りは速いが、大グループの書き込みが重い。投稿を一つだけ保存し、読む時に各ユーザー向けの状態を合成すれば書き込みは軽いが、未読計算や権限変更が難しくなる。実装は両者を組み合わせることが多い。
端末は last_seen_seq を送り、サーバーはその後の差分、削除、編集、reaction、receiptを返す。途中のsequenceが飛んでいれば再取得する。長期間オフラインの端末にはスナップショットと差分を組み合わせる。
一つの有名配信や巨大グループへ書き込みが集中すると、会話単位の順序付けノードがホットスポットになる。シャーディングしやすいユーザーIDと、順序を守りたいconversation IDのどちらを分割キーにするかが難しい。
平均だけでは障害を隠す。少なくとも次を分けて測る。
E2EEでは本文をログへ出せない、出すべきでない。message ID、匿名化した経路、段階別時刻、エラー分類で診断可能性を作る。
| 誤解 | 実際 |
|---|---|
| チェックが付いたから相手が読んだ | 受付・配送・既読のどれかは製品定義次第 |
| Push通知が届けば本文も永久に届く | Pushは遅延・省略され得る。アプリの同期が正本 |
| WebSocketならメッセージは失われない | 接続の道具であり、永続化・ACK・再送はアプリ側の責任 |
| E2EEならサービスは何も知らない | 本文は守れても配送メタデータが残る場合がある |
| exactly-once APIを作れば重複しない | 境界をまたぐ厳密なexactly-onceは難しく、IDによる冪等化が中心 |
メッセージアプリの速さは、一つの高速な通信方式から生まれるのではない。先に描くUI、再試行できる送信、冪等な保存、順序番号、オンライン配信、Pushという呼び鈴、差分同期が重なって、ようやく「送ったら自然に届く」という一つの体験になる。