2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

200なのに届かない ― FCMのWebプッシュ通知が消える場所を切り分ける

2
Last updated at Posted at 2026-07-10

はじめに

「管理画面では送信成功。配信ログにもエラーはなく、ステータスは 200。なのに自分のスマホには何も表示されない。」

プッシュ通知を運用していると、この状況にぶつかることがあるかもしれません。
しかも、どこにもエラーが出ない。ログはきれい、APIは成功、ダッシュボードの数字も問題なし。
でもプッシュ通知は届かなかった...とても困るケースです。

これは「バグが見つからない」のではなく、エラーにならないまま静かに落ちている状態です。
FCM でプッシュ通知を運用する際は、次の落とし穴を常に意識しておきたいです。

「送信成功」は、郵便局が手紙を受け付けたという意味でしかありません。ポストに届いた、または読まれたとは限らないのです。

「送信成功」から画面表示までの間には、脱落しうる関門が何段も挟まっています。
しかも Web(Push API)特有の「届いた"後"の表示で死ぬ」パターンは、Android 前提・配送インフラ寄りの公式ドキュメントではごっそり抜けています。

この記事では、「プッシュ通知がどこで死んだのか」を切り分ける手順を組み立てます。
切り分けさえできれば調べる範囲は一気に狭まり、「これは自分のコードのせいではない」と見極めて公式に委ねる判断もできるようになります。

対象読者: 自分のサイトにFCMでWebプッシュ通知を導入していて、「送ったはずなのに届かない」の調査方法を知りたいエンジニア。チュートリアルは動かした前提で進めます。

1. 通知が届くまでの流れと、最初の切り分け

原因を当てにいく前に、送信リクエストからユーザーの目に入るまで、通知が通る流れを押さえましょう。

送信サーバー
  │  ① send(送信リクエストを投げる)
  ▼
FCM
  │  ② 受理 ← ここが「送信成功」。郵便局が手紙を受け付けた地点
  ▼
プッシュサービス(ブラウザベンダーの配信網)
  │  ③ 端末へ配送
  ▼
端末 / ブラウザ
  │  ④ Service Worker の push イベントが発火 ← 手紙がポストに届いた地点
  ▼
Service Worker
  │  ⑤ showNotification() で通知を組み立てて表示
  ▼
画面(ユーザーが見る/気づく)← 手紙が開封された地点

③のプッシュサービスは Chrome や Firefox などブラウザベンダーが持つ配信網です。
FCM はこの標準の仕組み(Web Push)に乗っかって端末まで運んでもらうので、FCM がリクエストを受け取り、ブラウザにプッシュ通知を表示させる流れになります。

④の push イベントとは、プッシュ通知を受け取ったとき、 Service Worker が発火させるイベントです。
「通知が端末に届いた」瞬間そのものであり、「pushが発火したか」が配送系と表示系を分ける境界線になります。

大事なのは、受理(②)より後は、どの段でも通知が静かに脱落しうるということ。
送信リクエストそのものが弾かれれば FCM がエラーを返しますが、受理された後はどこも声を上げません。
しかもどの段で落ちたかによって、調べる場所も、そもそも自分に直せる問題なのかも、まったく変わってきます。

最初の切り分け ―「push は発火したか」

そこで、最初に確かめる問いを1つだけ決めます。

Service Worker の push イベントが発火したか。

これが「配送系の問題」か「表示系の問題」かを分ける境界線です。

send → 受理(200) → プッシュサービス → 端末 ─┃─→ push発火 → 表示 → ユーザー
                                        ┃
      【 届く"前" に死ぬ = 配送系 】       ┃       【 届いた"後" に死ぬ = 表示系 】
      トークン失効 / TTL切れ /             ┃        OS設定 / showNotification失敗 /
      ブラウザ未起動 など、宛先・インフラ側   ┃        自分の Service Workerの不具合など
  • push が発火しない → 配送系。Service Worker の外側の問題で、公式ドキュメントの守備範囲です
  • push は発火するのに画面に出ない → 表示系。設定・OS・自分の Service Worker の問題で、公式が手薄な領域です

この問いが効くのは、push発火する/しないの二択で、通知が届かなかったときに調べる範囲を半分に絞れるからです。

発火を確かめる観測コードを、push の入口に置く

では「push が発火したか」をどう知るか。
答えはService Worker の push ハンドラに観測コードを仕込むことです。
サーバーへ小さいリクエストを投げる、いわゆる beacon(ビーコン)ですね。

self.addEventListener("push", (event) => {
  // waitUntil: 中の非同期処理が終わるまで Service Worker を眠らせないための宣言
  event.waitUntil(handlePush(event));
});

async function handlePush(event) {
  // ★ 切り分け用のビーコン:まず「push が発火した」ことだけを記録する
  //    コツは2つ。ほかの処理より "先" に呼ぶ/レスポンスを待たず(await せず)次へ進む
  reportBeacon("push_received");

  const data = parsePayload(event); // ← ここで失敗しても、push はもう発火している

  await self.registration.showNotification(data.title, { body: data.body });
}

// ビーコン:どこまで来たかを、レスポンスを待たずに送る
// navigator.sendBeacon() を使わないのは、Service Worker では使えないため
function reportBeacon(name) {
  fetch("/beacon", {
    method: "POST",
    body: JSON.stringify({ event: name }),
    keepalive: true, // Service Worker が停止しても送り届けてもらう
  }).catch(() => {}); // 送信が失敗しても本処理は止めない
}

※サンプルコードのため、エラーハンドリング等は省略しています。

ペイロード解析やトークン取得より手前で呼んでおけば、その後の処理がどこで静かに失敗しても、「push は発火した」という事実だけは必ず残せます。
届かない通知に出くわしたら、まず「ビーコンは来ているか?」を見るだけで、配送系か表示系かを切り分けられます。

なお、ビーコン自体もネットワークや広告ブロッカーで落ちることがあるので、「来ない=100%配送系」と断定はできません。それでも切り分けの第一歩としては十分に強力です。

2. 届く"前"に死ぬ ― 配送系

ビーコンがまったく来ないなら、通知は Service Worker にたどり着く前に消えています。境界線の左側、配送系です。
ここで起きることは大きく3つ。

  • ① 宛先が「空き家」(トークン死):ユーザーがサイトデータを消した/PWA をアンインストールした/ストレージを自動削除された、などで FCM トークンは簡単に無効になります
  • ② 端末がそのとき受け取れない:オフライン、デスクトップでブラウザ未起動など。プッシュサービスは一定期間預かりますが、TTL(有効期限)を過ぎれば破棄されます
  • ③ 購読ごと無効:VAPID キー(Web Push の認証鍵)を差し替えると、それまでの購読はすべて無効になります

いずれも push が発火するより手前の話なので、Service Worker のコードを見ても手がかりは出ません。
対処する場所もそれぞれ別(トークン管理、送信側設定、ユーザー環境)で、深掘りは公式にゆずります。
トークンのライフサイクル(保存・失効検知・サーバー同期)や、TTL・collapse・オフライン時の挙動は、公式のブログやドキュメントが詳しいです。

3. 届いた"後"に死ぬ ― 表示系

ビーコンは来ている。つまり push は発火した。なのに画面には出ない――ここからが本題です。
境界線の右側、表示系。原因の多くは、自分の Service Worker かユーザー側の設定にあります。

まず、この右側をもう一段切り分けます。

showNotification() まで到達できたのか?

  • 到達していない → 自分の Service Worker の問題
  • 到達したのに見えない → OS・ブラウザ・ユーザー設定の問題

「通知が作られていない」― 静かに死ぬコード

push ハンドラは、通知を表示するまでに何段も処理を挟みます。ペイロードのJSON解析、トークン読み出し、表示オプションの組み立て、最後に showNotification()。この途中で失敗すると通知は作られません。

try/catch で受けて console.error に出すのは自然な書き方ですが、Service Worker のログは専用の開発者ツールをつないでいない限りどこにも残りません。
本番でそれを開いている人はいないので、console.error は実質どこにも残らず、サーバーにもログは届きません。

対策は切り分け用ビーコンと同じ発想です。失敗しうる地点それぞれに、「なぜ表示に至らなかったか」を理由付きで送ります

async function handlePush(event) {
  reportBeacon("push_received"); // reportBeacon の定義は前掲と同じ

  let data;
  try {
    data = parsePayload(event);
  } catch {
    reportBeacon("parse_failed"); // ← 解析で死んだことを記録
    // Service Workerは push を受けたら必ず通知を出す義務があるため、失敗時はフォールバック通知を試みる
    // ※エラー時の文言は "…" をサービスに合わせて設計する
    await self.registration.showNotification("", { body: "" });
    return;
  }
  // この後 data で通知を組み立てる。
  // トークン取得や showNotification も、同じように成否を個別に記録する
}

こうすれば「push は来た(ビーコンあり)が parse_failed で止まった」とサーバー側で分かります。

「通知が作られたのに見えない」― OSと設定の壁

理由付きビーコンは、失敗だけでなく成功も記録しておくのがコツです。
showNotification() の直後に「作れた」を表すビーコン(push_queued)を送れば、それが来ているかどうかで「作られていない側」か「作られたのに見えない側」かを切り分けられます。

「通知が作れた」のに画面に出ない――ここから先は OS とブラウザの領域です。

  • OSのおやすみ/集中モード:通知は作られているのに、バナーや音が抑制される
  • OS・ブラウザ側の通知オフ:ユーザーが設定でサイト通知を切っている
  • tag の上書き:同じ tag の通知は後から来たもので上書きされる。短時間の連投は最後の1つしか残らない
  • 通知数の制限:OSやブラウザが同時表示数を制限し、古いものが折りたたまれる

このあたりが「出したのに見えない」の原因として有力な候補です。

「表示できた」は「見られた」ではない

表示系でいちばん誤解されやすい点です。
showNotification() の成功は、通知をOSの通知キューに積めたという意味でしかありません。
ユーザーが実際に目にしたかまでは、Service Worker からは分かりません。

つまり push_queued の数(表示数)は、実際に見られた数の上限値として扱うべきものです。
おやすみモードや通知オフで抑制されたぶんは、表示数には乗っていてもユーザーには見えていません。
これはブラウザ Push の構造的な制約で、「表示数は出ているのに反応がない」ときは、この「表示≠閲覧」のギャップを疑ってください。

4. 手を動かすデバッグ

Chrome の DevTools と、これまで仕込んできたビーコンを使って、実際にデバッグしてみましょう。

push を手動で発火させる

毎回サーバーから実際の通知を送るのは大変ですよね。実は DevTools から push イベントを手動で発火できます。

  1. DevTools を開き、Application タブ → Service Workers を選ぶ

スクリーンショット 2026-07-02 18.40.12.png

  1. 対象の Service Worker の Push 入力欄に、自分のハンドラが解析できる形の JSON を入れて「プッシュ」を押す
{ "title": "新着メッセージ", "body": "テスト通知です" }
  1. 通知が届く

これでサーバーを介さず push ハンドラを動かせます。
ペイロードをわざと壊せば parse_failed の経路もその場で再現できます。

Service Worker のログを見る

本番では消えるログも、手元で手動発火させている間なら見られます。
ただし Service Worker のログはページのコンソールに出ないので、専用の開発者ツールを開きます。

  1. chrome://serviceworker-internals/ にアクセスし、対象の Service Worker の inspect をクリック
  1. 別ウィンドウで開く専用の開発者ツールの Console タブに、ハンドラ内の console.log が流れます。

ビーコンのログを読む ― どこで通知が消えたか

通知が届かなかった場合、サーバー側に届いたビーコンをこう読みます。

届いているビーコン 読み取れること
何も無い 配送系。push すら発火していない(宛先・インフラ側)
push_received だけ push は発火したが、通知を作る前に死んだ(自分の Service Worker)
push_receivedparse_failed など どの処理で死んだかが名指しで分かる
push_receivedpush_queued(=「作れた」) 通知は作れた。見えないなら OS・設定側

「どこまで来て、どこで途切れたか」が分かれば、あとは該当箇所を見にいくだけです。

配送系(ビーコンが何も来ない)と分かったら、宛先が生きているかをストレージで確認します。
FCM の SDK はトークンや購読情報を IndexedDBfirebase を含む名前のDBが目印)に保存しているので、DevTools の Application → IndexedDB で「トークンがそもそも入っているか」「サーバー登録済みのものと食い違っていないか」を見てみましょう。
空だったり古いままなら「宛先が空き家」の線が濃厚です。

最小の計測セット

最後に、これだけは仕込んでおきたい最小のビーコンです。

  1. push_receivedpushハンドラの入口(切り分けの起点)
  2. push_queuedshowNotification() の直後(OSのキューに積めたかどうか)
  3. 失敗理由parse_failed / token_failed など、死にうる地点ごと

これで「届かない」が起きたとき、配送系か・自分の Service Workerか・OS設定かをログを見て切り分けることができます。

まとめ

「送ったのに届かない」は単一の原因ではなく層の問題です。
通知は何段もの関門を通り、そのどこでも静かに脱落しうる。だからどこで死んだかを切り分けることが大事です。

重要なのは push イベントが発火したかです。
発火していなければ配送系(多くは公式の領域)、発火しているのに出ないなら表示系(公式が手薄な領域)です。
まずは push_received を判別するビーコン1つ仕込んでみましょう。
それだけで、通知が届かなかったとき、調べる範囲が半分になります。

ビーコンやDevToolsをうまく活用し、問題の切り分け・デバッグを行っていきましょう。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?