RORK LABEN
ACQUISITION — Rorkが初の買収を実施。macOSでネイティブSwiftアプリを生成するPaperlineを取得しましたFUNDING — Left Lane Capital主導の$15Mシードは、AI時代のモバイルアプリの作り方と収益化の再定義に充てられますGROWTH — Rork Maxはローンチから3日でARR $1.5Mに到達し、2週間で年間売上を倍増させたとされますENGINE — Rork MaxはClaude Code+Claude Opus 4.6駆動。Web初のSwiftビルダーとしてXcodeを置き換えますSPLIT — 通常RorkはReact Native(Expo)、Rork MaxはネイティブSwiftでAppleエコシステム全域が対象ですPRICING — 無料で開始でき、有料は月25ドル〜、Rork Maxは月200ドルですACQUISITION — Rorkが初の買収を実施。macOSでネイティブSwiftアプリを生成するPaperlineを取得しましたFUNDING — Left Lane Capital主導の$15Mシードは、AI時代のモバイルアプリの作り方と収益化の再定義に充てられますGROWTH — Rork Maxはローンチから3日でARR $1.5Mに到達し、2週間で年間売上を倍増させたとされますENGINE — Rork MaxはClaude Code+Claude Opus 4.6駆動。Web初のSwiftビルダーとしてXcodeを置き換えますSPLIT — 通常RorkはReact Native(Expo)、Rork MaxはネイティブSwiftでAppleエコシステム全域が対象ですPRICING — 無料で開始でき、有料は月25ドル〜、Rork Maxは月200ドルです
記事一覧/開発ツール
開発ツール/2026-06-25上級

通知が一部のユーザーにだけ届かない — Rork(Expo) アプリの到達率を診断して詰める設計

Rork で生成した Expo アプリで、プッシュ通知が一部のユーザーにだけ届かない問題を、送信から表示までの段に分けて診断し、到達率を計測して詰めるための設計を共有します。トークンの失効処理、APNs/FCM の失敗コード記録、優先度とメッセージ種別、特定ユーザーの切り分け手順まで実装込みで解説します。

Rork451Expo105プッシュ通知18到達率APNs3FCMトークン管理通知設計

プレミアム記事

通知が一部のユーザーにだけ届かない

個人開発のアプリで、ある告知をプッシュで送ったとき、開封数が想定よりかなり低く出たことがあります。送信ログ上は「全員に送った」ことになっていました。けれど数人から「通知が来ていない」と連絡があり、自分の予備端末でも届いていないものがありました。送ったのに届かない——この曖昧な状態を、当時の私は「APNs が不調なのだろう」くらいでしか説明できませんでした。

あとから分かったのは、「届かない」は1つの現象ではないということです。トークンが古い、サーバが受理を拒否した、プラットフォームが低優先度で間引いた、端末が表示しなかった——どれも結果は同じ「届かない」ですが、原因も対処もまるで違います。通知を送信から表示までの段に分けて観測し、どこで落ちたかを切り分けて到達率を詰めるための設計を、実装込みで共有します。

「届かない」を段に分解する

最初にやるべきは、ひとつの「届かない」を、独立して落ちうる段に分けることです。各段に観測点を置けば、漠然とした不調が「この段で何件落ちた」に変わります。

落ちる主な理由確認する信号
1. トークン取得許諾されていない/トークン未登録端末の許諾状態・サーバの登録有無
2. サーバ受理失効トークン・ペイロード不正APNs/FCM の応答コード
3. プラットフォーム配信低優先度の間引き・スロットリング優先度設定・メッセージ種別
4. 端末表示強制終了・通知オフ・サイレント処理受信ハンドラの到達ログ
5. 開封気づかれない/関心がない開封イベント

ほとんどの開発者は段5(開封率)だけを見ています。けれど「届かない」の多くは段2〜4で起きていて、そこを観測していないと、開封率の低さが「内容が悪い」なのか「そもそも届いていない」なのか区別できません。以下、落ちやすい段から順に潰していきます。

失効トークンを掃除する(最も静かな殺し屋)

実運用で最も多い「届かない」の原因は、サーバが古いトークンへ送り続けていることです。ユーザーがアプリを消す・再インストールする・OS を更新するとトークンは変わります。古いトークンへ送ると、APNs/FCM は受理時または後続のフィードバックで「無効」と返しますが、それを記録して掃除しないと、無効な宛先へ延々と送り続けることになります。

トークンは「登録」だけでなく「失効処理」までを一対にして設計します。

// register-token.ts — 端末側: トークンを登録し、変化を検知して更新する
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
 
export async function registerPushToken(userId: string) {
  if (!Device.isDevice) return; // シミュレータは実トークンを取れない
 
  const { status } = await Notifications.getPermissionsAsync();
  let finalStatus = status;
  if (status !== "granted") {
    finalStatus = (await Notifications.requestPermissionsAsync()).status;
  }
  if (finalStatus !== "granted") {
    // 段1で落ちる: 許諾なし。サーバ側で「送れない宛先」と記録する
    await fetch("https://api.example.com/push/unregister", {
      method: "POST",
      body: JSON.stringify({ userId, reason: "denied" }),
      headers: { "Content-Type": "application/json" },
    });
    return;
  }
 
  const token = (await Notifications.getExpoPushTokenAsync()).data;
  await fetch("https://api.example.com/push/register", {
    method: "POST",
    body: JSON.stringify({ userId, token, platform: Device.osName }),
    headers: { "Content-Type": "application/json" },
  });
}

サーバ側では、送信のたびに返ってくる失敗コードを必ず記録し、無効トークンを止めます。Expo のプッシュ API を使う場合、レシートに DeviceNotRegistered が返ったトークンは即座に無効化します。

// send.mjs — サーバ側: 送信し、失敗コードでトークンを掃除する
import { Expo } from "expo-server-sdk";
const expo = new Expo();
 
export async function sendBatch(messages) {
  const chunks = expo.chunkPushNotifications(messages);
  const stats = { accepted: 0, rejected: 0, invalidTokens: [] };
 
  for (const chunk of chunks) {
    const tickets = await expo.sendPushNotificationsAsync(chunk);
    tickets.forEach((t, i) => {
      if (t.status === "ok") {
        stats.accepted++;
      } else {
        stats.rejected++;
        // 段2で落ちた。理由コードを必ず残す
        if (t.details?.error === "DeviceNotRegistered") {
          stats.invalidTokens.push(chunk[i].to); // 後で DB から削除
        }
        console.error(`reject: ${t.details?.error} token=${chunk[i].to}`);
      }
    });
  }
  // 無効トークンを掃除(次回からこの宛先には送らない)
  await purgeTokens(stats.invalidTokens);
  return stats;
}

ここで acceptedrejected を集計値として残すのが要点です。「全員に送った」ではなく「受理 N 件・拒否 M 件・うち失効 K 件」と分かれば、段2での損失が数値になります。私はこの3つを送信のたびにログに残し、失効トークンの割合が跳ねていないかを見ています。割合が急に上がったときは、OS の大型更新やアプリの再インストール増が背景にあることが多いです。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
「通知が届かない」を送信→受理→配信→表示→開封の段に分解し、どの段で落ちたかを切り分けるための観測点を設計から示します
失効トークンの掃除・APNs/FCM の失敗コード記録・優先度とメッセージ種別の使い分けを、私が個人開発の通知運用で実際に回している実装のまま共有します
到達率を数値で計測する受信側の計装と、特定ユーザーから「届かない」と言われたときに5分で原因を絞る診断手順まで1本にまとめました
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-06-16
Expo のプッシュ通知トークンを本番で取りこぼさないために整えたこと
Rork で生成した Expo アプリに再訪促進のプッシュ通知を入れたところ、配信数がアクティブ数より明らかに少ない事態に直面しました。原因はトークンの取りこぼしと失効放置でした。取得から更新、サーバー保存、失効掃除までの運用設計を実装コード付きで記録します。
開発ツール2026-06-14
Rork アプリのプッシュ許可、いきなり出して断られていませんか — 事前許諾で取りこぼしを減らす設計
Rork(Expo)で作ったアプリの起動直後に OS のプッシュ許可ダイアログを出すと、断られた瞬間にもう二度と出せなくなります。一度断られると再表示できない iOS の仕様を踏まえ、自前の事前許諾画面で許可率を底上げする設計を、実装コード込みで整理しました。
開発ツール2026-06-13
通知の許可は一度しか聞けない — Rork アプリにソフトアスク(事前確認)を実装して opt-in を底上げする
iOS の通知許可は一度拒否されると二度とダイアログを出せません。Rork(Expo)アプリで、起動直後に system prompt を撃つのではなく自前のソフトアスク画面を一枚挟み、価値が伝わった瞬間に許可を求める実装を expo-notifications で組みます。Android 13 の POST_NOTIFICATIONS、拒否後の設定誘導、opt-in 計測まで通します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →