金色の配線パターンが広がる基板の接写
技術解説

Node.jsのSMSアラート、送信成功でも届いていない落とし穴

目次を見る

注文の異常検知やインフラ障害を検知したときに、SMSでアラートを飛ばす仕組みを組んでいるバックエンドエンジニアやSREに向けた内容です。AWS SNS(AWSが提供するメッセージ配信サービス)やSMS APIを使ってアラート送信を実装している場合、「APIが200を返した=相手に届いた」と誤解していないか、一度確認してみる価値があります。

結論から言うと、SMS送信APIのレスポンスが成功でも、それは「キャリア(携帯電話会社)に送信依頼を受け付けてもらえた」ことを示すだけです。実際に相手の端末に届いたかどうかは、別途ステータスを確認しないとわかりません。この差を見落とすと、障害通知が二重送信されたり、無効な宛先に送り続けたり、逆にエスカレーション(一次通知が届かない場合に上位の連絡手段に切り替える仕組み)が動かなかったりします。

何が起きるか:送信成功と配達完了の混同

典型的な事故パターンは3つあります。

  • 重複送信: プロセスが再起動やタイムアウトで送信処理をやり直し、同じアラートが2回届く
  • 無効宛先への送り続け: 退会済み・番号変更済みの宛先に何度も送信を試みてコストと信頼性が悪化する
  • エスカレーション遅延: 一次連絡が実際には届いていないのに、次の担当者への切り替えが発火しない

これらはすべて「APIのレスポンスコード」と「実際の配達状態」を同一視した設計から生まれます。

SMS送信の多くは、送信依頼を受け付けるエンドポイント(例: POST /v1/sms/send)と、配達状況を確認するエンドポイント(例: GET /v1/sms/status/{id})が分かれています。前者のレスポンスだけを見て処理を完了扱いにすると、後者を一度も呼ばないまま「送った」と記録してしまいます。

なぜ起きるか:ポーリング前提の設計とプル型ステータス

原因を分解すると、まず「配達確認がプル型(こちらから定期的に問い合わせる方式)である」という仕様理解の不足があります。Webhook(イベント発生時に相手からHTTPで通知してくれる仕組み)でプッシュ通知してくれるSMS APIもありますが、シンプルなAPIではステータスを自分でポーリングして取りに行く必要があります。これを知らずに実装すると、送信呼び出しの成功だけを頼りに次の処理へ進んでしまいます。

次に、リトライ処理とアラート発行の意味を混同している問題があります。HTTP 429(レート制限超過)を受け取った際の再送は、あくまで「通信レイヤーの復旧」であって「新しいアラートを作ってよい許可」ではありません。ここで冪等性キー(同じ処理を何度実行しても結果が1回分になるようにする識別子)を使わずに再送すると、同じ注文異常の通知が複数回届くことになります。

さらに、無効な宛先の扱いを「あとでまとめてクリーンアップするバッチ処理」に任せている設計も原因になります。宛先が無効だとプロバイダ側が判定した時点で、アプリケーション側の受信者台帳(recipient registry)を即座に更新しないと、次のアラートも同じ宛先に送られ続けます。これはマーケティングメールの配信停止とは性質が違い、運用上の状態(operational state)として扱うべきものです。

自分のプロジェクトが該当するか確認する方法

以下の観点でコードとインフラ設定を見直してみてください。

  • 送信処理のコードで、APIレスポンスの2xx受信直後にDBへ「送信完了」と記録していないか(ステータス確認ジョブが別途存在するか)
  • 冪等性キー(Idempotency-Key ヘッダーなど)を送信リクエストに付与しているか、また業務イベントIDから決定的に生成しているか
  • リトライ処理にexponential backoff(再試行の間隔を段階的に広げる方式)とRetry-Afterヘッダーの尊重が実装されているか
  • 無効宛先を検出した際に、受信者台帳のフラグを更新するコードパスが存在するか、それとも次回バッチ待ちになっているか
  • AWS SNSを使っている場合は、SNSのデリバリーステータスログ(CloudWatch Logsへの配信ステータス記録機能)を有効化しているか

AWS SNSであれば、コンソールの「Text messaging (SMS)」設定画面から、配信ステータスログの有効化状況とサンドボックス制限(未検証環境でのSMS送信数上限)を確認できます。サンドボックス状態のままだと、本番運用に必要な送信数を確保できていない場合もあるため、あわせて確認しておく価値があります。

対策の手順

1. アラート意図(intent)をイミュータブルなレコードとして永続化する

業務イベントID、宛先、テンプレートバージョン、エスカレーション期限をひとまとめにしたレコードを、送信前にDBへ書き込みます。これがアラートの「元帳(ledger)」になります。

# 概念的なテーブル例
# alert_intents(event_id PK, recipient, template_version, deadline_at, idempotency_key, created_at)

2. 送信とステータス確認を分離した状態機械にする

送信直後は「accepted(受理済み)」、ステータスAPIの結果で「delivered」「failed」「unknown」のいずれかに遷移させます。各観測結果はタイムスタンプ付きで追記型(append-only)で記録し、上書きしません。

3. 冪等性キーを業務イベントIDから決定的に生成する

# 例: イベントIDとテンプレートバージョンから冪等性キーを生成
idempotency_key=$(echo -n "${event_id}:${template_version}" | sha256sum | cut -d' ' -f1)

このキーを使い回すことで、ワーカープロセスが再起動して同じ処理を再実行しても、実際の送信は1回に保てます。多くのSMS APIやIdempotency-Key対応APIでは、24時間程度のデフォルト重複排除ウィンドウが設けられていることがあるため、アラートの再送許容期間がそれを超える場合は自前のストアでキーを長期保持してください。

4. エスカレーション期限を過ぎたらデッドレターキューへ回す

ステータス確認を続けても配達確認が取れないまま期限を過ぎたら、そのアラートを別キューに移し、次の連絡手段(電話・別担当者への通知など)へ切り替えます。国・地域ごとにキャリアの遅延特性は異なるため、ポーリング間隔は一律の数値を決め打ちせず、実際に利用するキャリア地域とプロバイダのステータス仕様から個別に決めるのが安全です。

5. 無効宛先はその場で抑制(suppress)する

プロバイダのドキュメントで定義された「恒久的な配達不能」を示すステータスを受け取ったら、受信者台帳を即座に更新し、再送を止めます。あわせて判定根拠のログを保持し、再有効化には監査可能な操作を要求する設計にしておきます。

6. AWS SNSを使い続けるか、専用SMS APIに切り替えるかの判断基準

既存のクラウドメッセージング基盤(SNSでメール・プッシュ通知・SMSをまとめて扱っている構成)の一部としてSMSを扱うなら、SNSを使い続ける方が運用コストは低く済みます。一方、テンプレート文言の正確な管理や、配達ステータスへの直接的なアクセスが必要な場合は、専用SMS APIを使った小さなクリティカルアラート専用ワーカーを別立てする方が制御しやすくなります。1分未満での即時エスカレーションが要件なら、Webhookでステータスを即座にプッシュしてくれるプロバイダを優先的に検討してください。

まとめ

SMSアラートの信頼性は、送信APIのレスポンスコードではなく、配達ステータスの追跡と冪等性の設計で決まります。

  • 送信成功と配達完了は別のイベントとして扱い、ステータス確認ジョブを必ず用意する
  • 冪等性キーを業務イベントIDから生成し、リトライと新規送信を区別する
  • 無効宛先は検出した瞬間に受信者台帳を更新し、次回バッチ待ちにしない

まずは自分のアラート送信コードで、APIレスポンス受信直後に「送信完了」と記録していないかを確認するところから始めてみてください。

参考

AWS SNS and Dedicated SMS APIs for Critical Node.js Alert Delivery

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。