あるアプリのクラッシュフリー率が、リリース直後に 99.7% から 99.2% まで落ちたことがありました。0.5 ポイントの低下です。このとき私は、次の機能追加を止めて原因調査に入るべきか、それともよくある一時的な揺れとして様子を見るべきか、しばらく決められませんでした。99.2% という数字が「危険」なのか「許容範囲」なのか、自分のなかに基準がなかったからです。
2014 年から個人開発を続けて 12 年、累計 5,000 万ダウンロードを超えたあたりから、私はこの「壊れにくさをどう判断するか」という問題にずっと付きまとわれてきました。アプリが 1 本なら感覚で十分です。けれど壁紙・癒し系のアプリを 6 本並行で運用していると、どのアプリにどれだけの時間を割くか、どれを伸ばしてどれを畳むかを、毎週のように判断しなければなりません。この記事は、クラッシュフリー率を「良い・悪い」の二値で眺めるのをやめ、エラーバジェットという予算として扱うようにした設計と、その数字を撤退・投資の判断に変える運用を、実際の数値とコードの両方からまとめたものです。
クラッシュフリー率を二値で見ると判断がぶれる
クラッシュフリー率を「99% を超えていれば OK、割ったら対応」という閾値で運用していた頃、私の判断は驚くほど一貫しませんでした。同じ 99.2% でも、機嫌のいい朝には「まだ大丈夫」と機能追加を続け、疲れている夜には「まずい」と全部止めて調査に入る。判断が体調に左右されていたわけです。
問題は、閾値が「いまの瞬間の値」しか見ていないことにあります。クラッシュフリー率は毎日揺れます。DAU の少ないアプリなら、1 人が連続でクラッシュしただけで 0.3 ポイント動くこともあります。瞬間値で機能凍結を判断すると、ノイズに振り回されて開発が止まったり、逆に継続的な悪化を見逃したりします。
ここで効くのが、SRE の世界で使われる SLO(Service Level Objective)とエラーバジェットの考え方です。「クラッシュフリー率 99.5% を目標にする」と決めたら、残りの 0.5% は使ってよい失敗の予算だと捉えます。予算が残っているうちは機能追加を続けてよい。予算を速く使い切りつつあるなら、その速度(バーンレート)に応じて手を打つ。判断の対象が「いまの値」から「予算の消費ペース」に変わるだけで、ぶれが劇的に減りました。
6 アプリに同じ SLO を当てはめてはいけない
最初に犯した失敗は、6 本すべてに「クラッシュフリー率 99.5%」という同じ目標を置いたことです。これはうまくいきませんでした。フラッグシップの壁紙アプリと、月数千ダウンロードの小さな癒し系アプリでは、1 件のクラッシュが持つ意味がまったく違うからです。
私はいま、収益貢献度と DAU 規模の 2 軸でアプリを 3 ティアに分け、ティアごとに SLO を変えています。
- Tier 1(主力・DAU 数万): クラッシュフリー率(ユーザー基準)99.5% / ANR 率 0.47% 未満。AdMob 収益の大半を担うので最も厳しく
- Tier 2(成長中・DAU 数千): 99.0% / ANR 率 0.6% 未満。伸ばしている最中なので、品質と機能追加速度のバランスを取る
- Tier 3(成熟・DAU 数百〜千): 98.5%。これ以上投資しないと決めたアプリ。バジェットを割っても基本は静観し、割り続けたら撤退判断の材料にする
ここで大事なのは、**SLO は「達成すべきノルマ」ではなく「どこに時間を使うかを決める道具」**だという点です。Tier 3 のアプリが SLO を割っても、私はすぐには動きません。むしろ「Tier 3 が継続的にバジェットを使い切る = もう手をかける価値が薄い」というシグナルとして、撤退の判断に使います。SLO の目的は完璧な品質ではなく、有限の時間を 6 本にどう配分するかを機械的に決めることなのです。
エラーバジェットとバーンレートを計算する
実装は薄く保ちました。Firebase の Crashlytics データは BigQuery エクスポート経由で日次のクラッシュフリーセッション数・総セッション数を取り出し、それを Cloudflare Workers の Cron Trigger で毎朝集計して KV に保存しています。Crashlytics の管理画面の数字を毎日 6 本ぶん目視するのは現実的ではないので、計算は完全に自動化しました。
エラーバジェットの基本式はシンプルです。SLO が 99.5% なら、許容される失敗は全体の 0.5%。その 0.5% に対して、実際に発生した失敗が何割を消費したかが「バジェット消費率」になります。
// 28 日ローリング窓でのエラーバジェット計算
interface DailyHealth {
date: string;
totalSessions: number;
crashedSessions: number;
}
interface BudgetResult {
slo: number; // 例: 0.995
budgetTotal: number; // 許容される失敗セッション数
budgetSpent: number; // 実際の失敗セッション数
budgetRemaining: number;
consumedRatio: number; // 0.0〜1.0+(1.0 超で予算超過)
crashFreeRate: number;
}
function computeErrorBudget(days: DailyHealth[], slo: number): BudgetResult {
const totalSessions = days.reduce((s, d) => s + d.totalSessions, 0);
const crashedSessions = days.reduce((s, d) => s + d.crashedSessions, 0);
// 許容失敗数 = 総セッション × (1 - SLO)
const budgetTotal = totalSessions * (1 - slo);
const budgetSpent = crashedSessions;
const consumedRatio = budgetTotal > 0 ? budgetSpent / budgetTotal : 0;
return {
slo,
budgetTotal,
budgetSpent,
budgetRemaining: Math.max(0, budgetTotal - budgetSpent),
consumedRatio,
crashFreeRate: totalSessions > 0 ? 1 - crashedSessions / totalSessions : 1,
};
}
瞬間値ではなくバーンレートを見るために、直近 1 日と直近 7 日の消費ペースを比べます。Google の SRE Workbook で紹介されている「マルチウィンドウ・バーンレート」の考え方を、個人開発向けにごく簡略化したものです。
// バーンレート = 「その期間で日割り予算の何倍を消費したか」
function burnRate(window: DailyHealth[], slo: number): number {
const totalSessions = window.reduce((s, d) => s + d.totalSessions, 0);
const crashed = window.reduce((s, d) => s + d.crashedSessions, 0);
const allowedFailRate = 1 - slo; // 1 日あたり許容失敗率
const actualFailRate = totalSessions > 0 ? crashed / totalSessions : 0;
return allowedFailRate > 0 ? actualFailRate / allowedFailRate : 0;
}
// burnRate = 1.0 → ちょうど予算どおりのペース
// burnRate = 2.0 → 予算を 2 倍速で消費(28 日窓を 14 日で使い切る)
// burnRate < 1.0 → 予算が貯まっていく(機能追加してよい状態)
私が公式ドキュメントから掴めず、運用してはじめて分かったのは、短い窓(1 日)と長い窓(7 日)の両方が高いときだけ深刻だということです。1 日だけ跳ねているのはたいてい単発の事故か特定端末の問題で、翌日には戻ります。7 日の窓まで高止まりして初めて「構造的な悪化」だと判断できます。片方だけで動くと、ノイズに反応して機能開発が止まります。
バジェット消費を「判断」に変えるゲート
数字が出たら、それを行動に変えるルールが要ります。私は次の判断ゲートを 6 アプリ共通で使っています。属人的な「なんとなく危ない」を排除するのが目的です。
type Action = "ship" | "freeze" | "hotfix" | "sunset-review";
function decide(
budget: BudgetResult,
burn1d: number,
burn7d: number,
tier: 1 | 2 | 3,
): Action {
// 1 日・7 日ともに高速消費 → 構造的悪化。即ホットフィックス
if (burn1d >= 3 && burn7d >= 2) return "hotfix";
// 予算を使い切った → 新機能を止めて信頼性回復に専念
if (budget.consumedRatio >= 1) return "freeze";
// Tier 3 が予算超過を続けるなら、伸ばす価値の再評価へ
if (tier === 3 && budget.consumedRatio >= 0.8) return "sunset-review";
// 予算に余裕(半分以上残)→ 機能追加を継続してよい
if (budget.consumedRatio < 0.5) return "ship";
// それ以外(消費 0.5〜1.0)→ 攻めすぎず維持
return "ship";
}
このゲートを入れてから、判断のスピードが上がりました。朝の集計で hotfix が返ったアプリだけを開けばよく、ship のアプリは安心して機能追加を進められます。6 本を毎朝なんとなく眺めて不安になる、という時間がほぼ消えました。
注意点として、freeze を返したアプリに対しては、機能ブランチを止めるだけでなく、Remote Config の feature_freeze フラグを立てて、リスクの高い実験的機能(新しい広告フォーマットの A/B など)をリモートから無効化しています。コードを触らずに「攻めの要素」だけを一時的に引っ込められるので、回復に集中できます。
長期運用で見えた、数字に出ない 3 つの落とし穴
半年ほどこの仕組みで 6 アプリを回してみて、SLO 運用の入門記事には書かれていない現実的な問題がいくつか見えてきました。
第一に、低 DAU アプリではバーンレートがほぼ常にノイズになります。Tier 3 の DAU 数百のアプリでは、1 人のユーザーの端末固有クラッシュで burnRate が一気に 5.0 を超えます。ここで hotfix を律儀に発動していたら時間がいくらあっても足りません。対策として、私は セッション数が一定(私の運用では 28 日窓で 3,000 セッション)に満たないアプリはゲートの判定対象から外し、「クラッシュ件数の絶対数」と「影響ユーザー数」だけを見るようにしました。統計的に意味のある母数がないところに比率を当てても、判断材料になりません。
第二に、クラッシュフリー率だけでは品質を捉えきれません。クラッシュしないけれど広告が出ない、課金ダイアログが開かない、といった無音劣化はこの指標にまったく現れません。私はエラーバジェットの判定に、クラッシュフリー率に加えて「起動から初回描画まで 4 秒以上かかったセッションの割合」を第二の SLI として組み込んでいます。落ちなくても遅ければユーザーは離れますし、AdMob の収益にも直結するからです。
第三に、SLO を厳しくしすぎると機能追加が永遠に始まりません。一度フラッグシップに 99.9% を設定したことがありますが、ローリング窓のバジェットがほぼ常に枯渇状態になり、新機能を一切出せなくなりました。99.5% に緩めたら、月に 2〜3 本の新機能を出しながらクラッシュフリー率も安定する、ちょうどよい点が見つかりました。SLO は高ければよいものではなく、機能追加の速度と釣り合う点を探すものだ、というのが半年の結論です。
状況別の推奨ターゲット
これから個人開発で複数アプリを運用する方に向けて、私が落ち着いた目安を共有します。あくまで壁紙・癒し系という比較的クラッシュの少ないジャンルでの数字なので、ゲーム等ではより緩めから始めるのが現実的だと思います。
- DAU 1 万以上・収益の主力: クラッシュフリー率 99.5% / 4 秒白画面率 2% 未満。マルチウィンドウのバーンレートで運用
- DAU 1,000〜1 万・成長中: 99.0%。バーンレートよりバジェット消費率(28 日窓)を主指標にすると揺れに強い
- DAU 1,000 未満・維持フェーズ: 比率での SLO は置かず、「週あたりの影響ユーザー数」と「クラッシュ絶対件数」で見る。
sunset-review の材料として消費傾向だけ記録する
両祖父が宮大工で、「手を動かすことが一つの信心だ」という感覚のなかで育ちました。アプリの信頼性に向き合う作業は、派手さこそありませんが、その丁寧に作り続ける感覚に一番近い仕事だと感じています。完璧を目指して立ち止まるのではなく、許される範囲の失敗を予算として受け入れながら、手を止めずに作り続ける。エラーバジェットという考え方は、私にとってその折り合いをつけるための道具になりました。
まず試すなら、いちばん収益貢献度の高い 1 本に SLO を 99.5% で置き、28 日窓のバジェット消費率だけを毎朝記録するところから始めてみてください。バーンレートや判断ゲートはそのあとで十分です。数字が「良い・悪い」から「あとどれくらい攻めてよいか」に変わる感覚を、一度味わっていただけたらと思います。実装の参考になれば幸いです。