定期実行とサーバー運用で起きた9つのこと:タイムアウト・時間予算・配備のたびの再実行・VPS 移行

English version

トレカダッシュ 開発の記録 / 9 件のまとめ

トレカダッシュの裏では、数百本の処理が毎時・毎日動いています。止まっても誰も気づかない、動きすぎても気づかない、という種類の不具合が多い場所です。ここには、定期実行とサーバーの運用で起きた 9 つのことと、直し方を、起きた順にまとめました。

それぞれの節は、もとは別々に公開していた記録です(2026 年 10 月から)。数字と出典は、その時の作業の記録のままです。テーマごとに読めるよう、1 つの記事にまとめ直しました(2026 年 11 月 7 日)。

Cloud Functions から月 4,300 円のサーバーへ移した:1 日 3,700 円の課金、EEA で全滅した Yahoo!、移行で踏んだ 5 つの罠

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の裏側は、もともと、Google の Cloud Functions(関数ごとに、動いた時間だけ課金されるサービス)で動いていました。2026 年 8 月 16 日に、無料のお試しの期間が終わると、8 月 17 日から、1 日約 3,700 円(月に直すと約 106,000 円)の実費がかかり始めました。

この記事は、そのコードをほとんど書き換えずに、月約 4,300 円のサーバー(VPS)に移した記録です。決めたこと、あとで直したこと、踏んだ罠を書きます。

数字は、2026 年 8 月 20 日の作業の記録から書いています。サーバーの住所や接続の情報は、書いていません。

なぜ移したか:待ち時間にも、料金がかかっていた

Cloud Functions の料金のうち、約 97% が、CPU の時間(vCPU 秒)でした。ところが、このアプリの関数は、ほとんど全部が、スクレイピング先や AI の返事を待っているだけです。その待ち時間も、フルに課金されていました。

定額のサーバーに移せば、この構造的な無駄は消えます。

Cloudflare Workers も検討しました。ただ、Firestore の管理用の道具(Admin SDK)が動かず、数百か所を別の API に書き換える必要があったので、やめました。VPS なら、今のコードが、そのまま動きます。

選んだもの、と、最初に間違えた場所

項目内容
提供元Hetzner Cloud
スペック2 vCPU、4GB メモリ、80GB SSD
場所シンガポール
料金月 €26.99(約 4,300 円)

最初は、ヘルシンキ(月 €5.99)に作りました。そして、作り直しました。

理由は、Yahoo! ショッピングの API が、欧州経済領域(EEA)とイギリスからのアクセスを遮断していることでした。ヘルシンキから叩くと、API の返事ではなく、「EEA およびイギリスからご利用のお客様へ」という案内のページが、HTTP 403 で返ります。15 ジャンルが全滅しました。これは、ボット対策ではなく、法的な理由の地域ブロックなので、何度試しても、通りません。シンガポールからは、HTTP 200 で、普通に取れます。

ヘルシンキのサーバーは削除し、かかった実費は €0.02(約 3 円)でした。

Hetzner の料金は、欧州の 3 拠点だけ、月 €7 の割引があります。さらに、いちばん安い種類(CX シリーズ)は、欧州でしか売っていません。つまり、「安い場所 = EEA = Yahoo! が死ぬ場所」という関係です。安さで、欧州に戻してはいけません。

シンガポールにした副次効果もありました。日本のサイトが近くなり、ポケモンカードの新着チェックが、52.7 秒から 15.8 秒(3.3 倍速)になりました。

コードを書き換えずに動かす:require を横取りする

実行役のプログラム(runner.js)を作りました。もとのコード(index.js)は、一切書き換えていません。

定期実行の定義 132 個を、手で書き写す代わりに、require(読み込み)を横取りして、firebase-functions を、自分のものに差し替えました。定期実行を登録する呼び出しを、「一覧に登録するだけの関数」にして、そのあとで、もとのコードを読み込みます。すると、全部の定義が、自動で集まります。回収した数は、次のとおりです。

定期実行 132 件 / HTTP のエンドポイント 256 件 / Firestore のトリガー 9 件

手書きしなくて、正解でした。ある定期実行の間隔は、`every ${X_CHECK_INTERVAL_MINUTES} minutes` というテンプレート文字列で書かれています。目で書き写していたら、間違えていたはずです。132 件を機械的に突き合わせて、全件一致を確かめました。

Firestore のトリガー(データの変化で動く関数)9 件は、VPS では発火しません。これらは、Cloud Functions 側に残しました(イベントで動くので、費用はほぼゼロです)。HTTP のエンドポイント 256 件も、アプリと管理画面が叩いているので、Cloud Functions に残しました。

移行で踏んだ 5 つの罠

1. 秘密の値は、5 本ではなく 15 本だった

移行前の記録には、「秘密の値は 5 本」と書いてありました。実際にコードの中の環境変数を全部数えると、15 本ありました。足りていなかった 10 本は、楽天、Google マップ、Yahoo!、X の認証、課金の連携、会員サイトのセッション、デバッグ用などです。

移し忘れは、エラーにならず、静かに、取得が 0 件になるので、気づきにくいものです。環境変数の名前を、コードから機械的に抜き出して数えます。

2. 秘密の値の中に、改行が入っていた

会員サイトのセッションの値は、「1 行に 1 つのサイト」の形式で、値そのものに改行が入っています。素直に設定ファイルへ書くと、2 行目以降が、別の変数として扱われ、4 つのサイトのうち 3 つが、静かに死にます。設定ファイルの読み込みを、改行の書き方(\n)に対応させました。

3. 書き込み禁止の試運転で、無限ループになった

楽天や Yahoo! の API には、呼び出しの間隔を守るための待ち(レート制限)があります。「最後に呼んだ時刻」を Firestore に書き、それが進むまで、ずっと待つ作りです。試運転で、書き込みを全部握りつぶすと、時刻が永遠に進まず、ループから抜けられません。(しかも、Cloud Functions のほうが 1.5 秒ごとに時刻を更新し続けるので、「他が更新したから、まだ待つ」を、無限に繰り返しました。楽天市場のジョブが、15 分以上帰ってこなくなって、発覚しました。)試運転でも、この待ちの記録だけは、書き込みを通すようにしました。

4. 「書き込みあり・通知なし」は、通知を静かに消す

通知をまとめて送る処理は、送れたと見なすと、送信待ちの一覧を削除します。通知だけを偽物にして、書き込みを許すと、送らずに消すことになり、通知が失われます。そこで、試運転の段階では、通知に関わる 7 つの定期実行を、VPS 側に、登録しないようにしました。外してよいのは、通知を ON にするときだけです。

5. 出口の IP が変わると、取得が 1 つ、静かに止まる

あるスーパーのサイトは、素のリクエストで取得していて、予備の手段がありませんでした。日本からは 200、シンガポールからは 403 になりました。全 75 ホストを、日本とシンガポールの両方から叩いて突き合わせると、「日本で見えて、シンガポールで見えない」のは 6 ホスト。そのうち 5 つは、もともと取得サービス経由か、予備つきで無事でした。壊れたのは 1 件だけでした。「シンガポールから 403」だけでは、壊れている証拠になりません。取得サービス経由かどうかを、必ず確かめます。

移行の順番:通知を二重に送らないために

  1. 段階 1:読むだけ(書き込みも通知もしない)で、取得の件数を、Cloud Functions と比べる。
  2. 段階 2:書くが、通知は Cloud Functions に任せる。
  3. 段階 3:Cloud Functions の、スケジュール関数 132 個だけを削除する。
  4. 段階 4:VPS を、通知 ON に切り替える。

どちらか一方だけが、通知を送る状態を、常に保ちます。同時に両方が送ると、利用者に、同じ通知が 2 回届きます。

分かったこと

待つだけの処理に、時間で課金されるサービスは、合わない。

ログに秘密のトークンが平文で残っていた:呼び忘れようがない唯一の出口で伏せる

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の裏側のログを調べていて、外部サービスの秘密の値(API のトークン)が、そのまま書かれているのを見つけました。2026 年 8 月 24 日のことです。

宿題として「期限なし、任意」に置いていた件でした。サーバーの実際のログを確認すると、実際に漏れていました。この記事は、原因と、「呼び忘れようがない出口で、伏字にする」直し方の記録です。

この記事は、2026 年 8 月 24 日の作業の記録から書いています。本物の値は、書いていません。

ログに、本物のトークンが出ていた

サーバーのログに、こういう行がありました(トークンの部分は、本物でした)。

WARN [再検証] 取得サービス経由での取得に失敗 [https://…]:
request to https://api.scrape.do/?token=<本物のトークン>&url=… failed, reason: read ETIMEDOUT

原因は、通信の部品(node-fetch)のエラーメッセージに、リクエストの URL が、そのまま入ることでした。URL の中のtoken=の部分に、秘密の値が含まれています。

前にも、同じことをして、直したつもりだった

2026 年 8 月 19 日にも、楽天の API の認証情報で、同じことが起きていました。そのとき、ログの文字列から秘密の値を伏せる関数(redactSecretsForLog)を作りました。ところが、この関数は、明示的に呼んだ場所にしか、効きません。取得サービスを呼ぶ場所は、50 か所以上あり、呼び忘れていたのです。

直し方:すべてのログが通る、唯一の出口で、伏せる

同じ直し方を繰り返さないよう、2 段構えにしました。

  1. ログの出口(fmt())で伏せる。サーバーの全部のログが、必ず通る、唯一の出口です。ここで伏せれば、呼び忘れようがありません。URL の中の、token、api_key、secret、applicationId、accessKey、affiliateId、setCookies などと、X の監視のログイン用クッキーの生の書き方の、両方を伏せます。
  2. 取得サービスを呼ぶ関数で、エラーを作り直す。呼び出し元に返す前に、伏せた文字列で、エラーを作り直します。関数が、データの変化で動く側(Cloud Functions のトリガー)でも使われるので、そちらのログにも効かせるためです。

実際のログの文字列 4 種で、伏せられることを確かめました。通常のログの行は、変化しないことも確かめました(deadline=2026-08-30 のような、無関係な = を、伏せてしまっては困ります)。

すでに出てしまったログは、どうするか

直しても、すでに出てしまったログには、本物のトークンが残っています。サーバーの管理者の権限でしか読めないので、緊急性は低いのですが、気になる場合は、サービスの管理画面でトークンを再発行して、サーバーの設定の値を差し替えます。この記録の時点では、これは、判断待ちの項目にしていました。

分かったこと

呼び忘れる場所が 50 あるなら、呼び忘れようがない場所で、1 度だけ行う。

タイムアウトを 3 回直しても止まらなかった:真犯人は、同じ鍵の中で動いていた 2 つ目の処理だった

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の、楽天ブックスの在庫を確かめる処理が、1 日に 17 回、タイムアウトしていました。タイムアウトは、処理が時間の上限(480 秒)までに終わらないことです。

この不具合を、3 回、直しました。それでも、止まりませんでした。真犯人は、3 回とも、見ていない場所にいました。この記事は、その調べ方の記録です。

この記事は、2026 年 8 月 25 日の作業の記録から書いています。

1 回目:取得に、タイムアウトが無かった

最初に見つけたのは、「取得に、時間の上限が無い」ことでした。外部のサイトからの返事が、永遠に来ないと、処理が永遠に待ちます。取得に、上限を付けました。ところが、1 日 17 回のタイムアウトは、止まりませんでした。

2 回目:「購入可能になった品の審査」で詰まっていた

次に疑ったのは、購入できるようになった品の、転売価格の審査でした。外部の API への問い合わせを含むので、ここで詰まる経路が残っていました。審査で見送った場合は、通知が 0 件のままなので、症状からは気づけません。

1 件ぶんの処理を、まるごと 1 つの関数(processOne())に切り出し、残り時間を上限にした Promise.race(先に終わったほうを取る)で包みました。デプロイして 7 分で、タイムアウトは 0 件。「確認: 12 件(時間切れのため、続きは次回)」と、正常に完了していました。

教訓は、「詰まりそうな 1 か所」だけに上限を付けても、足りないことでした。時間の予算は、ループの頭でしか見ていません。1 件の処理のどこかに、上限の無い待ちが 1 つでも残っていれば、そのまま、強制的な上限まで走り切ります。上限は、1 件ぶんの処理全体に掛けます。

ところが、タイムアウトは、止まっていませんでした。

3 回目:真犯人は、同じ鍵の中の、2 つ目の処理

3 回直しても止まらなかった理由は、上限を掛けた場所が、どれも本命ではなかったことでした。

真犯人は、同じ「二重実行を防ぐ鍵」の中で動いている、2 つ目の処理(runRevalidateRakutenBooksAvailability)でした。

1 つ目の処理(在庫のチェック)に、いくら上限を掛けても、2 つ目が、480 秒まで走り切っていました。ログには、1 つ目の完了のメッセージが出るので、「正常に終わっているのに、タイムアウトする」ように見えていたのです。

2 つ目にも、「残り時間」の上限を掛けました。打ち切っても、毎分実行されるので、取りこぼしにはなりません。デプロイして 15 分で、タイムアウトは0 件でした。

おまけ:毎回エラーになっていた、あるサイト

ついでに見つかった、別の不具合です。あるゲーム会社の通販サイトは、素の取得だと、リダイレクトの無限ループ(maximum redirect reached)になり、再検証のたびに、エラーのログが出ていました(1 日に十数回)。取得サービスの「昇格の記憶」では、強い設定が必要と、すでに判定済みのサイトでした。

直し方は、昇格の記憶に残っているホストは、素の取得を試さず、最初から取得サービス経由にすることでした。ホスト名を並べる方式は、採りませんでした。昇格の記憶は、実際の失敗から、自動でたまるので、新しいサイトが増えても、追加の作業が要りません。

分かったこと

タイムアウトを追うときは、まず、その関数が何を呼んでいるかを、全部数える。

「自動化できるものは全部自動化して」:まず数えると、修復処理 66 個が一度も定期実行から呼ばれていなかった。全部は載せず、6 個だけ載せた

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の開発で、ユーザーから、「自動化できるものは、全部自動化して」という指示がありました。

2026 年 8 月 28 日、まず、数えました。すると、直すための処理が 66 個、定期実行から一度も呼ばれていませんでした。この記事は、その数え方と、66 個を「全部は載せなかった」理由の記録です。

数字は、2026 年 8 月 28 日の作業の記録から書いています。

数えた:66 個が、「手で URL を叩かないと動かない」状態だった

HTTP のエンドポイント 319 個と、定期実行 184 個を、名前ではなく、関数の呼び出しで突き合わせました。名前が違っても、定期のジョブから呼ばれていることがあるからです。

結果、66 個が、手で URL を叩かないと動かない状態でした。人が「そろそろ回すか」と思い出すまで、直せるはずの記事が、直りません。

66 個を、一度に定期実行にはしなかった

66 個は、3 つに分かれました。

  1. 継続的に必要なもの(新しいデータが来るたびに要る)→ 定期実行に載せる。
  2. 一度きりの移行(backfill*、reclassify* など)→ 載せない。毎日走らせても無駄で、事故の余地が増えるだけ。
  3. 破壊的なもの、人の判断が要るもの(孤立した商品の削除、手動の紐付け直し など)→ 載せない。

新しい定期ジョブは、配備した瞬間に、一度走ります。載せる前に、全部、書き込まない下見(ドライラン)で、当たりを確かめました。

載せた 6 個

処理下見の結果
X の締切が空の記事を直す対象 32 件、更新 0 件
まとめ由来の店名を直す5 件(アカウントの宣伝文が、そのまま販売元になっていた)
送料が値段に入っているものを直す0 件
総称の商品への紐付けを直す0 件
分割した記事の題名を直す新設したもの
まとめ由来の締切を直す新設したもの

時刻は、ばらしました。同時に走らせると、同時実行の枠を食い合って、待ち行列が伸びるからです。定期実行は、184 個から 190 個になりました。

残りの候補(締切の修復、壊れた商品画像の修復、楽天のアフィリエイトリンクの修復)は、実行に 2 分以上かかり、下見が時間内に終わらなかったので、見送りました。確認せずに、載せません。

商品カタログは、すでに、全自動だった

「商品カタログも、自動にできるよね」という問いへの答えは、「もう、なっている」でした。15 ジャンルすべてに、公式カタログの取り込みがあり、1 時間前に更新されていました。記事と商品の紐付けも自動で、実測 1,415 件中 1,406 件が、紐付け済みでした。残りは、「公式サイトに、まだ載っていない、新しい弾」で、公式が出せば、自動で入ります。

待ち行列が、伸び続けていた

もう 1 つ、数えている途中で、見つけたことがあります。待ち行列が、81、86、91 と、45 秒で増え続けていました。

原因は、ジョブを積む処理(enqueue)が、「実行中か」しか見ておらず、キューで順番待ちしている分を、見ていなかったことでした。同時実行の上限に達している間に、「1 分おき」のジョブの時刻が、何度も来ると、同じジョブが、何重にも積まれます。

順番待ちの名前も覚えて、二重に積まないようにしました。待ち行列は、0 のままになりました。

分かったこと

自動化は、全部を載せることではない。数えて、仕分けて、確かめて、載せる。

「サーバーが落ちた。こちらからは復旧できない」は、誤りだった:止まっていたのは、SSH の受信だけだった

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」のサーバーに、午前 3 時 10 分から、つながらなくなりました。2026 年 8 月 28 日の朝のことです。私は、「サーバーが落ちた。こちらからは、復旧できない」と、記録に書きました。

この見立ては、誤りでした。10 時 15 分に復旧して、はじめて分かりました。この記事は、なぜ誤ったか、そして、そのときの作業が、何を生んだかの記録です。

この記事は、2026 年 8 月 28 日の作業の記録から書いています。

私が見たもの、と、私の結論

これらから、私は、「サーバーが止まった。こちらからは、復旧できない(提供元のコンソールでの再起動が要る)」と結論しました。

実際は:サーバーは、ずっと動いていた

10 時 15 分に、つながるようになって、確かめると、こうでした。

確かめたこと結果
サーバーのプロセスずっと動いていた(連続稼働 7 日 10 時間)
完了したジョブ4,100 件
定期実行 192 本正常に走り続けていた
遮断されていたものSSH の受信だけ

サーバーは、止まっていませんでした。外からの接続(SSH)だけが、遮断されていたのです。アプリの裏側の処理は、何も止まっていませんでした。

もう 1 つの誤り:「溜まった修復」は、私が作っていた

その間、私は、「毎時のはずの修復が、41 件も溜まっている」ことにも、気づいていました。これも、サーバーのせいだと、思っていました。

実際は、私が、ドライラン(書き込まない下見)を受け付けないエンドポイントを、点検のつもりで叩いたことで、生まれたものでした。点検のつもりが、本番の処理を、41 件も、動かしていたのです。

代わりの道:必要な処理だけを、別の場所へ配備した

サーバーに入れなくなったので、必要な点検と修復だけを、Cloud Functions 側へ、個別に配備して、作業を進めました(firebase deploy --only functions:名前)。定期実行には、一切、触れていません。サーバーの復旧後も、この HTTP のエンドポイントは残るので、点検に、そのまま使えます。

復旧した後の注意も、記録しました。まず、サーバーに、その日の変更を送ること。その日は、定期実行を 8 本足したので、配備の直後に、追い実行されます。どれも、手でドライランから実行まで済ませてあったので、走っても、実害はありません。

分かったこと

「つながらない」は、「止まっている」ではない。

「次回へ」と出続けていた 25 本の点検は、一度も走っていなかった:3 つの時間予算を、同じ開始時刻から数えていた

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の裏側には、記事の「販売の形式」を点検して直す処理があります。この処理の中に、「持ち回りで 25 本の点検を順に走らせる」部分がありました。

2026 年 9 月 11 日に、処理の各段の所要時間を測る計器をつけると、その 25 本は、一度も走っていなかったことが分かりました。ログには、「時間予算のため 18 件を次回へ」と出続けていました。この「18 件」は、一部ではなく、全部という意味でした。

数字は、2026 年 9 月 11 日の作業の記録から書いています。

3 つの予算が、同じ開始時刻から数えられていた

この処理は、3 段構えでした。それぞれに、時間の予算があります。

段予算実測
① 本体のループ420 秒425 秒を使い切る
② 毎回走る枠780 秒(通算)実際に使えるのは 360 秒。861 秒で終了
③ 持ち回りの 25 本700 秒(通算)②が 700 秒を超えるので、毎回 0 本

3 つの予算は、同じ開始時刻から数えられていました。①が 420 秒、②は通算で 780 秒、③は通算で 700 秒。一見、段ごとに余裕があるように見えます。しかし、②は「通算 780 秒」のうち、①が 425 秒を使ったあとの 360 秒しか使えません。そして、②が終わった時点(861 秒)で、③の「通算 700 秒」は、とうに過ぎています。③は、毎回、始まる前に、「予算切れ」と判断されて、0 本でした。

ログの「時間予算のため 18 件を次回へ」は、毎回、全部を次回へ回していました。「次回」は、永久に来ません。

直し方:勘で広げず、制限時間から割り付ける

予算の数字を、勘で広げるのは、やめました。代わりに、ジョブそのものの制限時間から、割り付ける形にしました。制限時間は 1 か所(REPAIR_SALE_TYPES_TIMEOUT_SEC)にだけ書き、各段の締め切りは、それに割合を掛けて出します。

段締め切り(制限時間に対する割合)
① 本体23%
② 毎回走る枠67%
③ 持ち回り93%

制限時間は、1,800 秒にしました。段の締め切りは、前の段の続きでなく、全体に対する割合なので、前の段が使い切っても、後ろの段の予算は残ります。

計器を足す:どの段が重いか、見えていなかった

もう 1 つの原因は、「12 本のうち、どれが重いのか」が分からないまま、上限だけを広げていたことです。そこで、1 ステップごとの所要秒数を、ログに出すようにしました。実測の、重い順です。

ステップ所要
通知済みの非表示記事に、代わりの記事を付ける119 秒
告知画像の QR から、応募先を探す102 秒
定価の目安の、単位を直す48 秒
定価の目安を、写す46 秒
応募先も応募方法も無い記事を直す41 秒

分かったこと

「次回へ」は、次回が来ると決まっているときだけ、意味がある。

同時実行の枠を増やす前に見るもの:2 コアのサーバーで、枠を 8 から 14 にした判断と、半分間違っていた前の結論

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の裏側は、1 台の小さなサーバー(2 コア、メモリ 3.8GB)で、200 を超える定期処理を動かしています。同時に走れる処理の数には、上限があります(同時実行の枠)。

2026 年 9 月 7 日、先着の監視を速くしようとして、処理の間隔を詰めたところ、いちばん速く見たい監視が、枠の順番待ちで遅くなりました。そこで枠を 8 から 11、さらに 14 に増やしました。この記事は、その判断と、判断を分けた「見るもの」の記録です。

数字は、2026 年 9 月 7 日の作業の記録から書いています。

枠が 8 では、足りなかった

先着の反応を上げるために、いくつかの処理の間隔を詰めました。すると、1 分おきの「瞬殺監視」(在庫が数分で消える先着を見る処理)が、見送られるようになりました。同時実行の枠 8 が、いっぱいになったからです。

枠 8(詰めた後)枠 11
順番待ちの数17〜290
同時に走っている数8 で張り付き6 / 11(余裕あり)
瞬殺監視の見送り6 回 / 10 分1 回 / 10 分
負荷(load average)1.621.24
メモリの使用1,161MB667MB

枠を 11 に増やすと、待ちは 0 になり、負荷もメモリも、むしろ下がりました。処理のほとんどは、通信の返事を待っているだけで、CPU を使っていません。上限は、CPU ではなく、枠だったのです。

ここで、枠の値は、サーバーの設定ファイル(runner.env)にだけ書かれていて、git(コードの履歴)には入りません。配備用のスクリプトが、このファイルを送らないからです。リポジトリを見ても、この変更は分かりません。作り直すときに忘れないよう、記録に残し、元の値の控えも作りました。

「2 コアだから、これ以上は無理」は、半分間違いだった

その後、人気の弾を速く回す枠と、最速で見る枠を足しました。すると、同時実行は 11 で張り付き、待ちが 15 件まで伸びて、瞬殺監視が 2 回見送られました(実質の間隔が 1.0 分から 1.4 分)。

調べると、こうでした。

このときは、「2 コアだから、枠を広げても全部が遅くなるだけ」と判断して、楽天の人気枠の頻度を減らしました。

足した後減らした後
同時実行11 に張り付き9〜11
順番待ち最大 150〜1
瞬殺監視の見送り2 回 / 10 分0 回
瞬殺監視の実質の間隔1.4 分1.0 分
負荷1.661.25

ところが、その後、この判断が半分間違いだったと分かりました。枠を握っている楽天系の処理は、API の 1.5 秒待ちで眠っているだけで、CPU を使っていません。枠を増やしても、「同時に眠れる数」が増えるだけで、CPU の取り合いにはなりません。枠を 11 から 14 にして測り直しました。

枠 11枠 14
瞬殺監視の見送り1 回 / 5 分0 回 / 6.7 分
瞬殺監視の実質の間隔1.3 分1.0 分
負荷1.251.25(変わらず)

判断を分けるのは、「CPU を使うか、待っているか」

枠を増やしてよいかは、処理がCPU を使うのか、待っているのかで決まります。

見る場所は 2 つです。順番待ちの数(ヘルスチェックの queued)が、いつも 1 以上なら、空きがありません。もう 1 つは、処理ごとの平均の所要秒数を、長い順に並べることです。楽天の API を使う長い処理が上位に並んでいたら、それが枠を握って眠っている犯人です。

この日だけで、同じ形の回帰を4 回出しました。Amazon の 5 分おき、あみあみの 5 分おき、楽天の人気枠のまとめ回し、楽天の人気枠の 10 分おき。速くしたつもりが、枠を食って、いちばん速く見たいものを遅くしました。

分かったこと

枠は、数を数えるのではなく、握っているものを見る。

監査 61 本を総点検して見つけた 4 つ:朝 4 時に 51 本が固まり、鍵の期限は 55 秒で、定価の補完は「入れない」と決めた

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の裏側には、異常を見つける監査が 61 本あります。2026 年 9 月 8 日の未明、その全部を流して、0 件ではなかった項目を、1 つずつ実データで確かめる作業をしました。

この記事は、そこで見つけた 4 つの話です。3 つは「直した」話で、1 つは「測ったうえで、あえて入れなかった」話です。

数字は、2026 年 9 月 7 日夕方〜8 日朝の作業の記録から書いています。

1. 1 日 1 回の処理 51 本が、朝 4〜5 時に固まっていた

朝 4 時 53 分の実測は、こうでした。

原因は、時刻の偏りでした。1 日 1 回の処理を数えると、4 時に 27 本、5 時に 24 本。一方、1 時と、10 時から 21 時までは、0 本でした。

直し方は、1 本ずつ時刻を書き換えることではありません。後から足した処理には、効かなくなるからです。時刻をずらす幅の仕組みそのものを直しました。ずらす幅が、一律 270 秒だったのを、間隔が 6 時間以上の処理は、最大 1,800 秒(30 分)まで散らします。27 本を 30 分に散らすと、約 67 秒おきになり、同時に走るのは 3〜5 本です。

30 分に留めたのは、次の時刻の枠にはみ出さないためです。「2 時に点検して、3 時に補完する」のような、時刻による順番を、壊したくありませんでした。

2. 「1 分おき」のはずの巡回が、1 回 181〜480 秒かかっていた

楽天市場の巡回は、1 分おきのつもりでした。実測では、1 回に 181〜480 秒かかっていました。害は、三重でした。

実質の間隔は、3.5〜6 分。しかも、タイムアウトで打ち切られると、毎回、同じ先頭のジャンルからやり直すので、後ろのジャンルに順番が回りません。

楽天ブックス側で実績のある、5 ジャンルずつの持ち回りに揃えました。

前後
1 回の所要181〜480 秒50〜83 秒
タイムアウト2 時間に 4 回0 回
1 ジャンルあたりの間隔3.5〜6 分3 分

ここで、もう 1 つ見つけました。「前回がまだ走っているなら、始めない」ための鍵(二重実行を防ぐ仕組み)の有効期限が、55 秒しかありませんでした。上限 480 秒、実測 181〜480 秒の処理では、実行係の見送りをすり抜けた回(手動の実行、再起動直後の追い実行)で、二重に走りうる状態でした。楽天 API を 2 倍使うので、先着の監視を直撃します。期限を、どちらも 480,000 ミリ秒に直しました。鍵の期限は、処理のタイムアウト以上にします。

3. 数えても直せない異常を、数え続けない

直す手段がないものを数え続けると、本物の異常が埋もれます。3 つを見直しました。

ただし、「取得 0 件」には、2 つの意味があります。(a) 条件に合う商品が無い(正常)。(b) ページの作りが変わって、1 件も読めない(故障)。区別できないと、本物の故障が埋もれます。(前日のポケモンセンターが、まさにこれでした。)そこで、走査した件数を、完了のメッセージに出すようにしました。

4. 測ったうえで、あえて入れなかった案

定価が分からない商品が 397 件ありました。定価が無いと、先着の通知が止まります。補完の処理は、実測すると、成果ゼロでした。

定価が未設定: 397 件
正規の店の価格がある: 51 件
2 店以上が一致: 0 件

「2 店以上が一致」という条件が、実質、一度も成り立たない条件になっていました。1 店でよしとすれば、51 件が埋まります。全部、公式カタログに定価が載っていない種類(マジックの輸入版など)でした。

それでも、入れませんでした。正規の店は、値引きをします。値引き後の価格を定価として記録すると、価格の審査の上限(定価 × 1.08 + 余裕)が、本物の定価より低くなり、正規の出品を、転売として消す方向に間違えます。10% 引きなら、本物の定価は 1.11 倍で、上限を超えます。しかも、消しすぎは、苦情が出ないので、気づけません。

入れるなら、先にやることを書き残しました。

  1. 51 件の価格を、公式サイトやメーカー希望小売価格と、目で突き合わせる(値引き後かどうかを、実際に確かめる。推測で係数を掛けない)。
  2. 1 店だけが根拠だと分かる印を付け、その印のときだけ、上限に余裕を足す。
  3. 入れたあと、非表示になった記事の数を、前後で数える。

分かったこと

偏りは、数えるまで見えない。入れない判断も、記録する。

配備のたびに、日次の処理が丸ごと走り直していた:48 時間で 477 回、その日の Gemini 費用の 46.6%

2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ」の裏側では、定期的に動く処理がたくさん動いています。その中に、「1 日に 1 回だけ動くはず」の処理がありました。ところが、ある日のログを数えると、その処理が 43 回動いていました。

この記事は、そこから見つかった、「配備(コードを入れ替えて再起動すること)のたびに、日次の処理が丸ごと走り直していた」不具合の記録です。

数字は、2026 年 9 月 1 日の作業の記録から書いています。

48 時間で、477 回の追い実行

処理の実行は、時刻になると動く仕組み(cron)で決まっています。再起動すると、その瞬間にあった予定は、消えます。そこで、起動したときに「前回の実行が古すぎる処理」を探して、取りこぼしとして追いかけて動かす仕組みを入れていました。この追い実行の判断は、「前回の実行時刻が、間隔の 1.05 倍より古いか」でした。

この日の 48 時間で数えると、追い実行は477 回ありました。中でも、記事に商品の紐付けを直す日次の処理は、1 日 1 回のはずが43 回動いていて、その日の Gemini(AI)の費用の46.6%、呼び出しにして 1,295 回を占めていました。やっていたのは、同じ記事に、もう一度 AI で値段を付けているだけで、得られるものは何もありません。

原因:「終わった」という記録が、残っていなかった

この処理は、時間の上限(540 秒)を超えると、「タイムアウトした」と記録されます。ただし、JavaScript には、動いている処理を途中で止める仕組みがないので、タイムアウトと記録されたあとも、本体は最後まで動き続けます。

問題は、「最後の実行時刻」の書き込みが、成功して終わったときだけだったことです。タイムアウトした回は、本体が最後まで終わっても、実行時刻が書かれません。すると、再起動のたびに、こう判断されます。

  1. この処理の最後の実行は、記録が無い(または古い)。
  2. 間隔の 1.05 倍より古いから、取りこぼしだ。
  3. いま走らせよう。

日次の処理が、再起動のたびに、丸ごと走っていました。配備を 1 日に何度かするだけで、日に 43 回です。

直し方:「タイムアウトを記録した」と「走らなかった」は別のこと

タイムアウトと記録されても、本体が最後まで終わったなら、その回は走ったものとして記録するようにしました。失敗して終わったときだけは、記録しません。

この直しは、時間の上限を下げる対策と、必ず対にしておく必要がありました。上限だけを下げても、実行時刻が残らなければ、追い実行は止まりません。

逆の問題:追いかけすぎても、足りなくても困る

この追い実行には、反対の問題もありました。8 月 31 日、私の配備が、朝 8 時ちょうどに重なりました。時刻に動く仕組みは、「その瞬間」にしか発火しないので、8 時のダイジェスト通知が、1 通も送られませんでした。

ダイジェストは、1 日に 5 回(6・8・12・18・21 時)です。平均の間隔は 4.8 時間で、追い実行の下限(6 時間)に届かず、「待てば次が来る」と見なされていました。ところが、利用者にとっては、その時刻を選んでいた人の 1 日ぶんが、消えたのです。

直し方は、利用者への届け物に関わる処理だけ、起動時に「直前の 55 分の予定を取りこぼしていないか」を見て、1 回流す形でした。予定から 10 分たつまでは、まだ走っていないだけかもしれないので、待ちます。

ここで、全部の処理に広げては、いけません。実データで数えたら、再起動のたびに 33 件が「取りこぼし」と判定され、ほとんどは再起動で中断されただけでした。全部を追いかけると、取得サービスや API の上限を、無駄に食います。足す基準は、「取りこぼすと、利用者への届け物が消えるか」です。

分かったこと

「記録が無い」は、「走らなかった」とは限らない。

それぞれの要点

ほかの記録は、Notes の一覧から、テーマごとに読めます。トレカダッシュそのものは、作品ページに書いています。

ほかの記録

コメント

感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。

押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはプライバシーポリシーに書いています。