RORK LABEN
EVENT — 本日9月9日、Apple が「Surprise and Shine」と題したイベントを開きます。日本時間では9月10日の未明2時からですEXPECT — iPhone 18 Pro と Pro Max、折りたたみ機、2nm プロセスの A20 Pro チップ、そして iOS 27 以下の配信日発表が見込まれていますWAIT — この記事を書いている時点ではまだ開催前です。噂の段階で書いたものと発表後に書いたものが混ざると、読む側には区別がつきませんMAX — Rork Max がネイティブ Swift を生成する以上、Apple の動きは他人事ではありません。標準の Rork は React Native、という線引きは繰り返し確認したいところですSIMULATOR — Rork Max はクラウド上の Mac でコンパイルし、ブラウザ内で動くストリーミングの iOS シミュレータで確認できます。Xcode も Mac の実機も要りませんSEASON — OS が新しくなる時期は、自動生成の足場がいちばん揺れます。便利さを謳う記事ほど、この揺れに触れないと不誠実になると感じていますEVENT — 本日9月9日、Apple が「Surprise and Shine」と題したイベントを開きます。日本時間では9月10日の未明2時からですEXPECT — iPhone 18 Pro と Pro Max、折りたたみ機、2nm プロセスの A20 Pro チップ、そして iOS 27 以下の配信日発表が見込まれていますWAIT — この記事を書いている時点ではまだ開催前です。噂の段階で書いたものと発表後に書いたものが混ざると、読む側には区別がつきませんMAX — Rork Max がネイティブ Swift を生成する以上、Apple の動きは他人事ではありません。標準の Rork は React Native、という線引きは繰り返し確認したいところですSIMULATOR — Rork Max はクラウド上の Mac でコンパイルし、ブラウザ内で動くストリーミングの iOS シミュレータで確認できます。Xcode も Mac の実機も要りませんSEASON — OS が新しくなる時期は、自動生成の足場がいちばん揺れます。便利さを謳う記事ほど、この揺れに触れないと不誠実になると感じています
記事一覧/開発ツール
開発ツール/2026-08-08上級

復旧させたのに 429 が止まらない — リトライのジッター設計を2,000端末で測り直しました

バックエンド復旧後も 429 が続いた原因を、2,000端末の離散シミュレータで測り直しました。指数バックオフ単体・上限値・±10%ジッター・Retry-After の遵守が、それぞれ何秒の遅れを生むかの実測値です。

リトライバックオフExpo203React Native236信頼性設計

プレミアム記事

個人開発のアプリが使っているバックエンドを、4分ほど止めた朝がありました。設定を1行変えるだけのつもりが、デプロイに失敗して落ちたままになった、という単純な話です。

慌てて戻して、ログを見ながらほっとしたのも束の間でした。復旧しているのに、429 が止まりません。

サーバは動いている。処理能力も戻っている。それでも数分のあいだ、返るのはエラーばかり。手元の端末で開き直しても同じでした。

原因は自分のリトライ実装にありました。しかも、教科書どおりに書いたつもりの部分が、いちばん効いていました。

腹落ちさせるには測るしかありません。手元で 2,000 端末ぶんの再接続を回せる小さなシミュレータを書いて、実装ごとに何秒の差が出るかを並べてみました。

何が起きていたのか

障害が起きると、その時間帯にアプリを開いていた端末が全部まとめて失敗します。失敗した瞬間がほぼ揃っているので、その後のリトライも揃います。

復旧した瞬間、揃った端末が一斉に戻ってくる。処理能力を超えるので 429 を返す。429 を受けた端末が、また揃ったタイミングで戻ってくる。

サーバは復旧しているのに、自分のクライアントが自分のサーバを塞ぎ続けている状態でした。

ここまでは、よく知られた thundering herd の説明そのものです。私が分かっていなかったのは、「では何をどれだけ入れれば解けるのか」という定量の部分でした。

測定台を作る

実サービスで実験するわけにはいかないので、離散時間のシミュレータを書きました。100ms を1tick として、端末の送信・サーバの処理・リトライの再スケジュールだけを回します。

条件は手元の構成に寄せています。端末2,000台、復旧は30秒時点、復旧後のサーバ処理能力は 120 req/sec。乱数は seed 固定の mulberry32 で、5つの seed の平均を取りました。

// herd.mjs — node herd.mjs で実行できます
const N = 2000;               // 同時に失敗した端末数
const RECOVER_MS = 30000;     // サーバ復旧時刻
const CAPACITY_PER_SEC = 120; // 復旧後の処理能力
const TICK = 100;             // 1tick = 100ms
const CAP_TICK = CAPACITY_PER_SEC * TICK / 1000;
const BASE = 1000;            // 基準待機 1s
 
// seed 固定の疑似乱数(結果を再現できるようにするため)
function mulberry32(a) {
  return function () {
    a |= 0; a = a + 0x6D2B79F5 | 0;
    let t = Math.imul(a ^ a >>> 15, 1 | a);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}
 
function run(nextDelay, seed) {
  const rand = mulberry32(seed);
  const next = new Float64Array(N);   // 次に送信する時刻
  const att  = new Int32Array(N);     // 試行回数
  const prev = new Float64Array(N);   // 直前の待機時間(decorrelated 用)
  const done = new Uint8Array(N);
  for (let i = 0; i < N; i++) { next[i] = rand() * 2000; prev[i] = BASE; }
 
  let sentAfterRecovery = 0, ok = 0, peak = 0, t99 = -1;
 
  for (let t = 0; t < 1200000; t += TICK) {
    const arrivals = [];
    for (let i = 0; i < N; i++) if (!done[i] && next[i] <= t) arrivals.push(i);
 
    const up = t >= RECOVER_MS;
    if (up) {
      sentAfterRecovery += arrivals.length;
      const qps = arrivals.length * 1000 / TICK;
      if (qps > peak) peak = qps;
    }
 
    // 復旧前は全滅、復旧後も処理能力を超えた分は 429 相当で弾かれます
    let budget = up ? CAP_TICK : 0;
    for (const i of arrivals) {
      if (budget >= 1) { budget--; done[i] = 1; ok++; }
      else {
        att[i]++;
        const d = nextDelay(att[i], prev[i], rand);
        prev[i] = d;
        next[i] = t + d;
      }
    }
    if (t99 < 0 && ok >= N * 0.99) t99 = t;
    if (ok === N) break;
  }
  return { sentAfterRecovery, ok, peak: Math.round(peak), t99 };
}

計測するのは3つに絞りました。復旧後にサーバへ届いた総リクエスト数、復旧後のピーク QPS、そして 99% の端末が通り終えるまでの時刻です。

平均応答時間を見ても意味がありません。この種の障害で読者が体感するのは、いちばん遅い層がいつ戻るかだからです。

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

この記事の続きを読む

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

この記事で得られること
指数バックオフをジッター無しで入れると、固定5秒間隔より99%完了が7倍遅くなる実測値
「Retry-After をそのまま守る」実装が最も遅かった理由と、下限として扱う書き換え方
decorrelated jitter と端末固定オフセットを組み合わせた、そのまま使える fetch ラッパー
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。
開発ツール2026-09-01
expo-paste-input で画像を貼り付けられる入力欄を作り、消える file:// を documents へ逃がす
チャット入力に画像やGIFを貼り付けられるようにする expo-paste-input の実装手順です。onPaste が返す file:// は一時ファイルで、送信までの間に消えることがあります。保存先を移す実装と、実機で確かめる順番をまとめました。
開発ツール2026-08-31
画面ロックから3分後、expo-audio の環境音は静かに止まっていました
expo-audio の音が画面ロックで止まる原因を、ビルド設定・オーディオセッション・ロック画面連携の三層に分けて切り分けます。Android で約3分後に止まるのは、ドキュメントに書かれた止まり方です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます