💸

Pi 5 で Claude Code を 40 日運用して、1 日 5 USD を 2 USD に下げた話

に公開

40 日間で何が起きていたか

4 月上旬、Raspberry Pi 5 の上で Claude Code を有料プランで回し始めた。複数の個人プロジェクト(データ分析、定期レポート、コード補助)を systemd user timer に載せて、24 時間自動で走らせる構成だ。

最初の 3 週間、ダッシュボードの USD コストはこんな推移だった。

4/09  1.82
4/12  2.41
4/15  3.18
4/18  3.94
4/22  4.51
4/26  4.88
4/29  5.07

頭打ちにならない。むしろ徐々に積み上がる。タスクが増えたわけではない。同じ timer が同じ時間に動いているだけだ。それなのに、conversation あたりの消費トークンが日に日に増えていた。

このまま行くと月 150 USD ペースで、個人プロジェクトのコストとしては看過できない水準になる。3 週間かけて原因を絞り込み、3 つの設定を入れたら 5/01 以降の平均が 2.04 USD/day になった。-60% の削減で、生産性は変わらない。

この記事はその記録だ。同じ「Claude Code を有料プランで毎日使うが、コストが伸び続ける」状況にいる人の助けになれば嬉しい。

観察したパターン: long conversation の罠

最初に疑ったのは「タスクが重くなった」だったが、ログを見るとそうではなかった。1 conversation あたりのトークン消費を時系列で並べると、傾向がはっきり出ていた。

  • conversation の経過時間が 30 分を超えたあたりから、context window の使用率が 70-80% に張り付く
  • そこから先、Claude Code は自動 compaction(古い文脈の圧縮)を繰り返す
  • compaction 自体がトークンを消費する。しかも cache に乗らない

つまり「長く動く conversation ほど、終盤の 1 ターンが高くつく」状態だった。

cache hit 率を見ると、より明確だった。

conversation 長 cache hit 率
0-10 分 64%
10-30 分 41%
30-60 分 23%
60 分超 12%

長い conversation は、文脈が頻繁に書き換わるので prompt cache が育たない。さらに compaction で context が変わるたびに、次のターンも cache miss になる。負の連鎖だ。

短く切ることが、品質を下げずにコストを下げる最短ルートだと判断した。

やったこと 1: timer を「タスク粒度」に分割

これまでは「朝のルーティーン」のような大きな単位で 1 つの timer を作り、その中で複数のことをやらせていた。たとえば「市況データ取得 → 分析 → レポート整形 → 通知送信」を 1 つの prompt で走らせていた。

これを 4 本の timer に分割した。

old: morning-routine.timer        (45 分かかる 1 本)

new: morning-fetch.timer          (8 分)
     morning-analyze.timer        (12 分)
     morning-report.timer         (6 分)
     morning-notify.timer         (3 分)

各 timer は独立した conversation として起動するので、必ず 0 から始まる。conversation が 30 分を超えることがほぼなくなり、cache hit 率も自然に戻った。

副次的な効果として、失敗の局所化が起きた。以前は途中の analyze で詰まると朝のルーティーン全体が止まっていたが、分割後は notify だけ独立して走るので「分析が古いまま通知だけ届く」状況を回避できる(前段の output が無いと skip するチェックを入れた)。

ただし注意点もある。粒度を細かくしすぎると、各 timer 間で context を渡すために結局ファイル経由で受け渡しが必要になり、書き込み・読み込みのオーバーヘッドが増える。今は「30 分を超えそうな処理だけ分割する」を目安にしている。

やったこと 2: prompt の冒頭で「単一目的」を宣言

Claude Code は丁寧な性格をしているので、prompt が曖昧だと「ついでにこれも見ておきますか?」と探索を始める。これが long conversation の温床だった。

たとえば「今日の市況データを取得して」だけだと、取得が終わった後で「ファイルが正しく書けたか確認します」「他のデータも揃ってるか見てみます」と進む。気持ちはありがたいが、自動実行には不要だ。

prompt の冒頭に、こう書くようにした。

このタスクは <X> のみを行う。完了の定義は <Y> が出力されること。
それ以外の探索・改善・追加調査は不要。<Y> が出たら止まること。

最初は「諦めが早すぎて失敗する」ケースが出た。たとえば取得した CSV が壊れていても「<Y> が出たから完了」で止まってしまう。なので「完了の定義」は具体的に書く必要がある。

完了の定義: data/today.csv が存在し、行数が 100 以上、
最終行の timestamp が今日の日付であること。

ここまで書けば、誤って早期終了することはほぼ無くなった。prompt の長さは 30 文字くらい増えるが、conversation 全体の長さは大幅に短くなる。

やったこと 3: timer ごとに max-budget-usd を設定

claude -p には --max-budget-usd という上限設定がある。これを timer ごとに、平均完了に必要な額の 1.5 倍を上限に設定した。

morning-fetch.service:    --max-budget-usd 0.30
morning-analyze.service:  --max-budget-usd 0.80
morning-report.service:   --max-budget-usd 0.40
morning-notify.service:   --max-budget-usd 0.20

これは費用の絶対上限であると同時に、Claude Code 内部で「予算が無くなりそう」を意識させる効果がある。実際、上限を設定した方が conversation が短く終わる傾向が出た(具体的に何が違うかは公開情報からは推測でしかないが、結果としては短くなった)。

注意点: bug fix のような探索的タスクには厳しすぎる上限を入れない方がいい。3 回試して 3 回とも予算切れで止まり、結局手動で続きをやる羽目になった日がある。日常タスク(取得・整形・通知)は厳しめ、探索タスクは緩めの 2 段構えにしている。

結果

3 つの設定を入れた 5/01 以降、ダッシュボードはこうなった。

5/01  2.18
5/05  2.04
5/09  1.97
5/13  2.11
5/17  1.94
  • 平均 USD/day: 5.07 → 2.04(-60%)
  • conversation の中央値長: 18 分 → 7 分
  • cache hit 率: 32% → 58%
  • timer の失敗率: ほぼ変化なし(0.8% → 0.9%)

生産性指標としては、生成される記事数・通知数・git commit 数のいずれも誤差レベルの変化に収まった。「同じ量の仕事を、半分のコストで」ができている。

まとめ

長い conversation がコストの根本原因だった。Claude Code の compaction を信用していたが、長くなった conversation は cache が育たず、compaction 自体も無料ではない。

対策は「conversation を短く切る」「目的を明確にして探索を抑える」「予算上限で 念を押す」の 3 つで、どれも特別なツールは要らない。systemd の構成ファイルと prompt の書き方を変えるだけだ。

同じように「Claude Code のコストが伸び続けている」状況なら、まずは自分の conversation がどれくらいの長さで動いているかを測ってみることをお勧めする。30 分を超えているものがあれば、そこに改善余地がある可能性が高い。

GitHubで編集を提案

Discussion