定期実行とサーバー運用で起きた9つのこと:タイムアウト・時間予算・配備のたびの再実行・VPS 移行
トレカダッシュ 開発の記録 / 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:読むだけ(書き込みも通知もしない)で、取得の件数を、Cloud Functions と比べる。
- 段階 2:書くが、通知は Cloud Functions に任せる。
- 段階 3:Cloud Functions の、スケジュール関数 132 個だけを削除する。
- 段階 4:VPS を、通知 ON に切り替える。
どちらか一方だけが、通知を送る状態を、常に保ちます。同時に両方が送ると、利用者に、同じ通知が 2 回届きます。
分かったこと
待つだけの処理に、時間で課金されるサービスは、合わない。
- 課金の単位を見る。待ち時間にも課金される構造だと、スクレイピングや AI の呼び出しは、割高になります。
- 置き場所には、法的な制約がある。安い場所が、使えない場所のこともあります。
- コードを書き換えずに移す道を探す。横取りと機械的な突き合わせで、手書きの誤りを避けました。
- 設定の数は、機械で数える。5 本ではなく 15 本でした。
- 段階を踏んで、通知の二重送信を避ける。
ログに秘密のトークンが平文で残っていた:呼び忘れようがない唯一の出口で伏せる
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 段構えにしました。
- ログの出口(
fmt())で伏せる。サーバーの全部のログが、必ず通る、唯一の出口です。ここで伏せれば、呼び忘れようがありません。URL の中の、token、api_key、secret、applicationId、accessKey、affiliateId、setCookiesなどと、X の監視のログイン用クッキーの生の書き方の、両方を伏せます。 - 取得サービスを呼ぶ関数で、エラーを作り直す。呼び出し元に返す前に、伏せた文字列で、エラーを作り直します。関数が、データの変化で動く側(Cloud Functions のトリガー)でも使われるので、そちらのログにも効かせるためです。
実際のログの文字列 4 種で、伏せられることを確かめました。通常のログの行は、変化しないことも確かめました(deadline=2026-08-30 のような、無関係な = を、伏せてしまっては困ります)。
すでに出てしまったログは、どうするか
直しても、すでに出てしまったログには、本物のトークンが残っています。サーバーの管理者の権限でしか読めないので、緊急性は低いのですが、気になる場合は、サービスの管理画面でトークンを再発行して、サーバーの設定の値を差し替えます。この記録の時点では、これは、判断待ちの項目にしていました。
分かったこと
呼び忘れる場所が 50 あるなら、呼び忘れようがない場所で、1 度だけ行う。
- 明示的に呼ぶ対策は、呼び忘れで漏れる。50 か所のうち 1 か所でも忘れれば、漏れます。
- 唯一の出口で、伏せる。全部のログが通る場所に、1 つだけ置きます。
- 伏せすぎないことも、確かめる。通常のログが変化しないことを、試します。
- すでに出たものは、直しても消えない。再発行するかを、判断します。
- 外部の部品のエラーメッセージを、信じない。URL をそのまま含むことがあります。
タイムアウトを 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 件ずつ API を叩く。
- 楽天の API には、1.5 秒の間隔の制限がある。
- 時間の上限が、一切ない。
1 つ目の処理(在庫のチェック)に、いくら上限を掛けても、2 つ目が、480 秒まで走り切っていました。ログには、1 つ目の完了のメッセージが出るので、「正常に終わっているのに、タイムアウトする」ように見えていたのです。
2 つ目にも、「残り時間」の上限を掛けました。打ち切っても、毎分実行されるので、取りこぼしにはなりません。デプロイして 15 分で、タイムアウトは0 件でした。
おまけ:毎回エラーになっていた、あるサイト
ついでに見つかった、別の不具合です。あるゲーム会社の通販サイトは、素の取得だと、リダイレクトの無限ループ(maximum redirect reached)になり、再検証のたびに、エラーのログが出ていました(1 日に十数回)。取得サービスの「昇格の記憶」では、強い設定が必要と、すでに判定済みのサイトでした。
直し方は、昇格の記憶に残っているホストは、素の取得を試さず、最初から取得サービス経由にすることでした。ホスト名を並べる方式は、採りませんでした。昇格の記憶は、実際の失敗から、自動でたまるので、新しいサイトが増えても、追加の作業が要りません。
分かったこと
タイムアウトを追うときは、まず、その関数が何を呼んでいるかを、全部数える。
- 1 つのスケジュール関数の中に、処理が 2 つ以上あることがある。3 回とも、1 つ目だけを見ていました。
- 上限は、1 件ぶんの処理全体に掛ける。詰まりそうな 1 か所だけでは、足りません。
- 「正常に終わっているのに、タイムアウト」は、別の処理を疑う。完了のメッセージは、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 つに分かれました。
- 継続的に必要なもの(新しいデータが来るたびに要る)→ 定期実行に載せる。
- 一度きりの移行(
backfill*、reclassify*など)→ 載せない。毎日走らせても無駄で、事故の余地が増えるだけ。 - 破壊的なもの、人の判断が要るもの(孤立した商品の削除、手動の紐付け直し など)→ 載せない。
新しい定期ジョブは、配備した瞬間に、一度走ります。載せる前に、全部、書き込まない下見(ドライラン)で、当たりを確かめました。
載せた 6 個
| 処理 | 下見の結果 |
|---|---|
| X の締切が空の記事を直す | 対象 32 件、更新 0 件 |
| まとめ由来の店名を直す | 5 件(アカウントの宣伝文が、そのまま販売元になっていた) |
| 送料が値段に入っているものを直す | 0 件 |
| 総称の商品への紐付けを直す | 0 件 |
| 分割した記事の題名を直す | 新設したもの |
| まとめ由来の締切を直す | 新設したもの |
時刻は、ばらしました。同時に走らせると、同時実行の枠を食い合って、待ち行列が伸びるからです。定期実行は、184 個から 190 個になりました。
残りの候補(締切の修復、壊れた商品画像の修復、楽天のアフィリエイトリンクの修復)は、実行に 2 分以上かかり、下見が時間内に終わらなかったので、見送りました。確認せずに、載せません。
商品カタログは、すでに、全自動だった
「商品カタログも、自動にできるよね」という問いへの答えは、「もう、なっている」でした。15 ジャンルすべてに、公式カタログの取り込みがあり、1 時間前に更新されていました。記事と商品の紐付けも自動で、実測 1,415 件中 1,406 件が、紐付け済みでした。残りは、「公式サイトに、まだ載っていない、新しい弾」で、公式が出せば、自動で入ります。
待ち行列が、伸び続けていた
もう 1 つ、数えている途中で、見つけたことがあります。待ち行列が、81、86、91 と、45 秒で増え続けていました。
原因は、ジョブを積む処理(enqueue)が、「実行中か」しか見ておらず、キューで順番待ちしている分を、見ていなかったことでした。同時実行の上限に達している間に、「1 分おき」のジョブの時刻が、何度も来ると、同じジョブが、何重にも積まれます。
順番待ちの名前も覚えて、二重に積まないようにしました。待ち行列は、0 のままになりました。
分かったこと
自動化は、全部を載せることではない。数えて、仕分けて、確かめて、載せる。
- まず、数える。66 個が、一度も呼ばれていませんでした。名前ではなく、呼び出しで突き合わせます。
- 仕分ける。継続的なものだけを載せます。一度きりの移行と、破壊的なものは、載せません。
- 載せる前に、下見で当たりを確かめる。新しい定期ジョブは、載せた瞬間に走ります。
- 時刻を、ばらす。同時に走らせると、待ち行列が伸びます。
- キューに積む前は、「待っているか」も見る。実行中だけを見ると、何重にも積まれます。
「サーバーが落ちた。こちらからは復旧できない」は、誤りだった:止まっていたのは、SSH の受信だけだった
2026 年 10 月 17 日の記録。
iOS アプリ「トレカダッシュ」のサーバーに、午前 3 時 10 分から、つながらなくなりました。2026 年 8 月 28 日の朝のことです。私は、「サーバーが落ちた。こちらからは、復旧できない」と、記録に書きました。
この見立ては、誤りでした。10 時 15 分に復旧して、はじめて分かりました。この記事は、なぜ誤ったか、そして、そのときの作業が、何を生んだかの記録です。
この記事は、2026 年 8 月 28 日の作業の記録から書いています。
私が見たもの、と、私の結論
- サーバーの、22 番(SSH)、80 番、443 番のポートが、全部、閉じていた。
- 経路を調べると(traceroute)、シンガポールの通信会社の基幹の回線までは届くが、その先で止まる。
- サーバーの提供元(Hetzner)の、状況のページには、その地域の障害の掲示は、無かった。
これらから、私は、「サーバーが止まった。こちらからは、復旧できない(提供元のコンソールでの再起動が要る)」と結論しました。
実際は:サーバーは、ずっと動いていた
10 時 15 分に、つながるようになって、確かめると、こうでした。
| 確かめたこと | 結果 |
|---|---|
| サーバーのプロセス | ずっと動いていた(連続稼働 7 日 10 時間) |
| 完了したジョブ | 4,100 件 |
| 定期実行 192 本 | 正常に走り続けていた |
| 遮断されていたもの | SSH の受信だけ |
サーバーは、止まっていませんでした。外からの接続(SSH)だけが、遮断されていたのです。アプリの裏側の処理は、何も止まっていませんでした。
もう 1 つの誤り:「溜まった修復」は、私が作っていた
その間、私は、「毎時のはずの修復が、41 件も溜まっている」ことにも、気づいていました。これも、サーバーのせいだと、思っていました。
実際は、私が、ドライラン(書き込まない下見)を受け付けないエンドポイントを、点検のつもりで叩いたことで、生まれたものでした。点検のつもりが、本番の処理を、41 件も、動かしていたのです。
代わりの道:必要な処理だけを、別の場所へ配備した
サーバーに入れなくなったので、必要な点検と修復だけを、Cloud Functions 側へ、個別に配備して、作業を進めました(firebase deploy --only functions:名前)。定期実行には、一切、触れていません。サーバーの復旧後も、この HTTP のエンドポイントは残るので、点検に、そのまま使えます。
復旧した後の注意も、記録しました。まず、サーバーに、その日の変更を送ること。その日は、定期実行を 8 本足したので、配備の直後に、追い実行されます。どれも、手でドライランから実行まで済ませてあったので、走っても、実害はありません。
分かったこと
「つながらない」は、「止まっている」ではない。
- 1 つの角度から、断定しない。ポートが閉じていても、プロセスは動いていることがあります。
- 別の信号を見る。通知が届いているか、データが更新されているか、アプリ側の状態。サーバーの中に入れなくても、外から見えるものがあります。
- 「復旧できない」と書く前に、「どこまで分かっているか」を書く。私が分かっていたのは、「SSH がつながらない」だけでした。
- 点検のつもりで、本番を動かさない。ドライランを受け付けない入口は、下見ができません。
- 入れないときの、別の道を持っておく。個別に配備できる場所が、役に立ちました。
「次回へ」と出続けていた 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 秒 |
分かったこと
「次回へ」は、次回が来ると決まっているときだけ、意味がある。
- 予算を、同じ開始時刻から数えると、後ろの段が飢える。前の段が使い切れば、後ろは 0 です。
- 「次回へ」のログを数える。毎回、全部が「次回へ」なら、それは「一度も走っていない」です。
- 上限は、勘で広げず、全体の制限時間から割り付ける。
- まず、各段の所要時間を測る。どれが重いか分からないまま、上限だけ広げても、直りません。
同時実行の枠を増やす前に見るもの: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〜29 | 0 |
| 同時に走っている数 | 8 で張り付き | 6 / 11(余裕あり) |
| 瞬殺監視の見送り | 6 回 / 10 分 | 1 回 / 10 分 |
| 負荷(load average) | 1.62 | 1.24 |
| メモリの使用 | 1,161MB | 667MB |
枠を 11 に増やすと、待ちは 0 になり、負荷もメモリも、むしろ下がりました。処理のほとんどは、通信の返事を待っているだけで、CPU を使っていません。上限は、CPU ではなく、枠だったのです。
ここで、枠の値は、サーバーの設定ファイル(runner.env)にだけ書かれていて、git(コードの履歴)には入りません。配備用のスクリプトが、このファイルを送らないからです。リポジトリを見ても、この変更は分かりません。作り直すときに忘れないよう、記録に残し、元の値の控えも作りました。
「2 コアだから、これ以上は無理」は、半分間違いだった
その後、人気の弾を速く回す枠と、最速で見る枠を足しました。すると、同時実行は 11 で張り付き、待ちが 15 件まで伸びて、瞬殺監視が 2 回見送られました(実質の間隔が 1.0 分から 1.4 分)。
調べると、こうでした。
- CPU は 2 コア。負荷は 1.66 で、2 コアの 83%。
- 楽天系の長い処理は、楽天の API の「1.5 秒待ち」で、枠を握ったまま眠っている。在庫の再検証 240 秒、楽天市場 181 秒、新規出品 110 秒、在庫 104 秒。
- CPU は使っていないのに、枠だけを消費する。いちばん見えにくい犯人でした。
このときは、「2 コアだから、枠を広げても全部が遅くなるだけ」と判断して、楽天の人気枠の頻度を減らしました。
| 足した後 | 減らした後 | |
|---|---|---|
| 同時実行 | 11 に張り付き | 9〜11 |
| 順番待ち | 最大 15 | 0〜1 |
| 瞬殺監視の見送り | 2 回 / 10 分 | 0 回 |
| 瞬殺監視の実質の間隔 | 1.4 分 | 1.0 分 |
| 負荷 | 1.66 | 1.25 |
ところが、その後、この判断が半分間違いだったと分かりました。枠を握っている楽天系の処理は、API の 1.5 秒待ちで眠っているだけで、CPU を使っていません。枠を増やしても、「同時に眠れる数」が増えるだけで、CPU の取り合いにはなりません。枠を 11 から 14 にして測り直しました。
| 枠 11 | 枠 14 | |
|---|---|---|
| 瞬殺監視の見送り | 1 回 / 5 分 | 0 回 / 6.7 分 |
| 瞬殺監視の実質の間隔 | 1.3 分 | 1.0 分 |
| 負荷 | 1.25 | 1.25(変わらず) |
判断を分けるのは、「CPU を使うか、待っているか」
枠を増やしてよいかは、処理がCPU を使うのか、待っているのかで決まります。
- 待っている処理が枠を握っているなら、枠を増やしてよい。負荷(
uptime)が上がらないことを確かめる。 - CPU を使う処理が枠を握っているなら、増やすと全部が遅くなる。
見る場所は 2 つです。順番待ちの数(ヘルスチェックの queued)が、いつも 1 以上なら、空きがありません。もう 1 つは、処理ごとの平均の所要秒数を、長い順に並べることです。楽天の API を使う長い処理が上位に並んでいたら、それが枠を握って眠っている犯人です。
この日だけで、同じ形の回帰を4 回出しました。Amazon の 5 分おき、あみあみの 5 分おき、楽天の人気枠のまとめ回し、楽天の人気枠の 10 分おき。速くしたつもりが、枠を食って、いちばん速く見たいものを遅くしました。
分かったこと
枠は、数を数えるのではなく、握っているものを見る。
- 「1 回の所要時間 ≧ 間隔」の処理は、枠を占有する。速くするほど、ほかが待ちます。
- 上限の正体を、測る。CPU なのか、枠なのか。負荷とメモリを見ます。
- 前の結論が、半分間違っていることがある。「2 コアだから無理」は、待ちの処理には当てはまりませんでした。
- gitに入らない設定は、記録に残す。サーバーにだけある値は、リポジトリを見ても分かりません。
- 速くするなら、何かを遅くする。どれを守るかを、先に決めます。
監査 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 分の実測は、こうでした。
- 同時実行の枠 14 が、いっぱいに張り付いている。待ちが 8。
- 6 本の処理が、上限の 540 秒でタイムアウトしている。
- 守りたい「瞬殺監視」と「Amazon の最速枠」が、1 時間に 9 回ずつ見送られている。
原因は、時刻の偏りでした。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 秒かかっていました。害は、三重でした。
- 同時実行の枠を、ほぼ常に 1 つ占有する。
- 480 秒の上限でタイムアウトする(2 時間に 4 回)。結果は捨てられる。
- その間ずっと、楽天の API の枠(1.5 秒に 1 回、全部の処理で共有)を握るので、先着の本命の瞬殺監視の取り分が減る。
実質の間隔は、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 つを見直しました。
- 「抽選なのに締切なし 77 件」を 0 件に。実データ 89 件の本文を全部調べると、締切らしい日付があるのは 5 件だけで、その 5 件も「抽選発表 9/5」「受け取りは 9/13 まで」のような、応募の締切ではありませんでした。店が書いていないだけで、直す手段がありません。本文に、締切の言い回しと日付が両方あるものだけを、異常として数えます。
- 「スクレイパー停止 3 件」を 0 件に。数えていたのは、処理ではなく、状態を記録しただけの項目(実行時刻を持たない)でした。本体は除外しているのに、監査だけが揃っていませんでした。
- ある店の「173 時間 0 件」は、正常でした。3 つのカテゴリを取ると、ページは取れていて(16 件と 6 件)、その全部が「ポイント交換専用」(0 円)でした。抽選や予約が、今は無いだけです。
ただし、「取得 0 件」には、2 つの意味があります。(a) 条件に合う商品が無い(正常)。(b) ページの作りが変わって、1 件も読めない(故障)。区別できないと、本物の故障が埋もれます。(前日のポケモンセンターが、まさにこれでした。)そこで、走査した件数を、完了のメッセージに出すようにしました。
4. 測ったうえで、あえて入れなかった案
定価が分からない商品が 397 件ありました。定価が無いと、先着の通知が止まります。補完の処理は、実測すると、成果ゼロでした。
定価が未設定: 397 件
正規の店の価格がある: 51 件
2 店以上が一致: 0 件
「2 店以上が一致」という条件が、実質、一度も成り立たない条件になっていました。1 店でよしとすれば、51 件が埋まります。全部、公式カタログに定価が載っていない種類(マジックの輸入版など)でした。
それでも、入れませんでした。正規の店は、値引きをします。値引き後の価格を定価として記録すると、価格の審査の上限(定価 × 1.08 + 余裕)が、本物の定価より低くなり、正規の出品を、転売として消す方向に間違えます。10% 引きなら、本物の定価は 1.11 倍で、上限を超えます。しかも、消しすぎは、苦情が出ないので、気づけません。
入れるなら、先にやることを書き残しました。
- 51 件の価格を、公式サイトやメーカー希望小売価格と、目で突き合わせる(値引き後かどうかを、実際に確かめる。推測で係数を掛けない)。
- 1 店だけが根拠だと分かる印を付け、その印のときだけ、上限に余裕を足す。
- 入れたあと、非表示になった記事の数を、前後で数える。
分かったこと
偏りは、数えるまで見えない。入れない判断も、記録する。
- 時刻の偏りを、数える。4 時に 27 本、10〜21 時に 0 本。
- 上限が見えない処理は、必ず何かを握る。鍵の期限は、タイムアウト以上に。
- 直せないものを、異常として数えない。数が合わないときは、数え方を疑う。
- 「0 件」の意味を、区別する。走査した件数を出す。
- 「入れない」と決めたら、理由と、入れる条件を残す。測って、成果が出ても、消しすぎる危険があれば、入れません。
配備のたびに、日次の処理が丸ごと走り直していた: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.05 倍より古いから、取りこぼしだ。
- いま走らせよう。
日次の処理が、再起動のたびに、丸ごと走っていました。配備を 1 日に何度かするだけで、日に 43 回です。
直し方:「タイムアウトを記録した」と「走らなかった」は別のこと
タイムアウトと記録されても、本体が最後まで終わったなら、その回は走ったものとして記録するようにしました。失敗して終わったときだけは、記録しません。
この直しは、時間の上限を下げる対策と、必ず対にしておく必要がありました。上限だけを下げても、実行時刻が残らなければ、追い実行は止まりません。
逆の問題:追いかけすぎても、足りなくても困る
この追い実行には、反対の問題もありました。8 月 31 日、私の配備が、朝 8 時ちょうどに重なりました。時刻に動く仕組みは、「その瞬間」にしか発火しないので、8 時のダイジェスト通知が、1 通も送られませんでした。
ダイジェストは、1 日に 5 回(6・8・12・18・21 時)です。平均の間隔は 4.8 時間で、追い実行の下限(6 時間)に届かず、「待てば次が来る」と見なされていました。ところが、利用者にとっては、その時刻を選んでいた人の 1 日ぶんが、消えたのです。
直し方は、利用者への届け物に関わる処理だけ、起動時に「直前の 55 分の予定を取りこぼしていないか」を見て、1 回流す形でした。予定から 10 分たつまでは、まだ走っていないだけかもしれないので、待ちます。
ここで、全部の処理に広げては、いけません。実データで数えたら、再起動のたびに 33 件が「取りこぼし」と判定され、ほとんどは再起動で中断されただけでした。全部を追いかけると、取得サービスや API の上限を、無駄に食います。足す基準は、「取りこぼすと、利用者への届け物が消えるか」です。
分かったこと
「記録が無い」は、「走らなかった」とは限らない。
- 実行の記録は、結果のあとに必ず書く。成功のときだけ書くと、タイムアウトした回が「未実行」に見えます。
- 追い実行は、少なすぎても多すぎても壊れる。届け物は追いかける。それ以外は、次の回を待つ。
- 配備のたびに何が走るかを、数える。48 時間の 477 回は、数えるまで見えていませんでした。
- 同じ記事に、同じ仕事を繰り返すと、費用が溶ける。その日の費用の 46.6% が、何も生まない呼び出しでした。
それぞれの要点
- Cloud Functions から月 4,300 円のサーバーへ移した:無料のお試しが終わると 1 日約 3,700 円(月約 10.6 万円)かかった。待ち時間にも課金される構造が原因で、コードをほぼ書き換えずにシンガポールの VPS(月約 4,300 円)へ移した。ヘルシンキでは Yahoo! が全滅、秘密の値は 5 本でなく 15 本だった。
- ログに秘密のトークンが平文で残っていた:通信の部品のエラーメッセージに URL がそのまま入り、取得サービスのトークンがログに出ていた。前に作った伏字の関数は、明示的に呼ぶ場所にしか効かず、呼び元は 50 か所以上。ログの唯一の出口で伏せる形に直した。
- タイムアウトを 3 回直しても止まらなかった:楽天ブックスの在庫チェックが 1 日 17 回タイムアウトした。取得の上限、1 件ぶんの上限と 2 回直しても止まらず、3 回目で、同じ鍵の中の別の処理(全件を取り直し、上限なし)が真犯人と分かった。
- 「自動化できるものは全部自動化して」:HTTP の 319 個と定期実行の 184 個を呼び出しで突き合わせると、66 個が手で叩かないと動かなかった。継続的なものだけを、下見のあとで 6 個載せた。キューに積む処理が順番待ちを見ておらず、待ち行列が伸び続けていたことも直した。
- 「サーバーが落ちた。こちらからは復旧できない」は、誤りだった:午前 3 時 10 分からサーバーに接続できなくなり、私は「止まった」と判断した。実際は、プロセスは 7 日以上動き続け、定期実行 192 本も正常で、遮断されていたのは SSH の受信だけだった。
- 「次回へ」と出続けていた 25 本の点検は、一度も走っていなかった:点検の処理は 3 段構えで、予算が同じ開始時刻から数えられていたため、持ち回りの 25 本は毎回 0 本だった。ログの「18 件を次回へ」は全部の意味だった。制限時間から割り付ける形に直し、各ステップの所要時間をログに出した。
- 同時実行の枠を増やす前に見るもの:先着の監視を速くしようと間隔を詰めると、1 分おきの瞬殺監視が枠の順番待ちで遅くなった。枠を 8、11、14 と変えて測った。上限の正体は CPU ではなく、楽天 API の 1.5 秒待ちで枠を握る処理だった。
- 監査 61 本を総点検して見つけた 4 つ:1 日 1 回の処理が 4 時に 27 本・5 時に 24 本集まって枠を塞いでいた。楽天の巡回は 1 回 181〜480 秒で、鍵の期限は 55 秒しかなかった。数えても直せない異常を数えるのをやめ、定価を 1 店で埋める案は、測ったうえで入れなかった。
- 配備のたびに、日次の処理が丸ごと走り直していた:1 日 1 回のはずの処理が 43 回走り、Gemini の費用の 46.6% を占めていた。タイムアウトした回は最終実行時刻が書かれず、再起動のたびに取りこぼし扱いされていた。逆に、配備が 8 時に重なって、ダイジェストが 1 通も届かなかった問題も。
ほかの記録は、Notes の一覧から、テーマごとに読めます。トレカダッシュそのものは、作品ページに書いています。
ほかの記録
- Xcode 15.2 で Firebase 12 が使えない(Swift 6.0 必須):Firebase 11.4.0 と AdMob 11.5.0 に固定した理由 — 古い Mac の Xcode 15.2 では、Firebase 12 以降と 11.12.0 以降は Xcode 16.2 が必須、AdMob 11.6.0 以降は MarketplaceKit で Xcode 15.3 が必須。Exact Version で固定した記録。
- ITMS-91064「無効なバイナリ」:NSPrivacyTracking が true なのに NSPrivacyTrackingDomains が空だった — ビルドが Apple の自動審査で ITMS-91064 により却下された。NSPrivacyTracking が true で、NSPrivacyTrackingDomains が空の配列だったのが原因。ドメインを補った経緯と、確認できていないこと。
- Firebase Auth:アプリを完全に終了すると、匿名アカウントが量産された(currentUser の復元待ち) — 「アプリを落とすと、また Apple のサインインを求められる」。管理画面を開くと、19人中17人が使い捨ての匿名アカウント。起動直後に currentUser を同期チェックしていた競争を、addStateDidChangeListener で直した記録。
コメント
感想や、記事の間違いの指摘は、ここへ。書くには、GitHub のアカウントが要ります。
押すと、giscus(giscus.app)と GitHub に接続します。押さなければ、接続しません。詳しくはプライバシーポリシーに書いています。