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

> 毎日・毎時動く処理と、それを載せるサーバーで起きた9つの記録。Cloud Functions から VPS への移行、3回直したタイムアウト、走っていなかった点検、配備のたびの再実行、ログの秘密。

- URL: https://koshinstudio.com/notes/scheduled-jobs/
- 公開: 2026-10-17
- 書いた人: Koshin（https://koshinstudio.com/about/）
- サイト: Koshin Studio

  English version (https://koshinstudio.com/en/notes/scheduled-jobs/)

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

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

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

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

  2026 年 10 月 17 日の記録。

iOS アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側は、もともと、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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側のログを調べていて、外部サービスの秘密の値（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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の、楽天ブックスの在庫を確かめる処理が、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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の開発で、ユーザーから、「自動化できるものは、全部自動化して」という指示がありました。

  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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」のサーバーに、午前 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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側には、記事の「販売の形式」を点検して直す処理があります。この処理の中に、「持ち回りで 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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側は、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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側には、異常を見つける監査が 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 件も読めない（故障）。区別できないと、本物の故障が埋もれます。（前日のポケモンセンター (https://koshinstudio.com/notes/silent-failures/#s4)が、まさにこれでした。）そこで、走査した件数を、完了のメッセージに出すようにしました。

### 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 アプリ「トレカダッシュ (https://koshinstudio.com/works/torecadash/)」の裏側では、定期的に動く処理がたくさん動いています。その中に、「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 の一覧 (https://koshinstudio.com/notes/)から、テーマごとに読めます。トレカダッシュそのものは、作品ページ (https://koshinstudio.com/works/torecadash/)に書いています。
