概要
claudelogプロジェクト(Claude Code会話ログをOllamaで要約するCLIツール)の開発中に、HeadersTimeoutErrorが環境変数の設定では解決しないという問題に遭遇しました。
原因はOllama側ではなく、Node.jsのfetch API(undici由来)のデフォルトタイムアウトにありました。本記事では、問題の切り分け、試した対策、最終的な解決策、そしてモデルサイズと精度のトレードオフについて記録します。
発生した問題
エラー内容
1HeadersTimeoutError: Headers Timeout Error2 at Timeout.onParserTimeout [as callback] (node:internal/deps/undici/undici:...)3 code: 'UND_ERR_HEADERS_TIMEOUT'発生状況
- 使用モデル: llama3.2:3b
- 処理内容: Claude Codeの会話ログ(数千行)をOllamaに送信して要約を生成
- タイミング: 長いログの要約時に発生。短いログでは発生しない
claudelogの用途
claudelogは、Claude Codeのセッションログを解析・要約するCLIツールです。ログファイルを読み込み、Ollamaのローカルモデルで要約を生成し、検索可能な形式で保存します。
試した対策と結果
対策1:OLLAMA_TIMEOUT環境変数(効果なし)
最初に試したのは、Ollamaの環境変数によるタイムアウト延長です。
1export OLLAMA_TIMEOUT=6000000 # 6000秒(100分)2ollama serve結果: 効果なし。同じHeadersTimeoutErrorが発生しました。
OLLAMA_TIMEOUT=6000000に設定してもまだタイムアウトするのは、エラーの発生箇所がOllamaではなかったためです。
対策2:Ollamaのkeep_aliveパラメータ(効果なし)
API呼び出し時にkeep_aliveパラメータを指定する方法も試しました。
1const response = await fetch('http://localhost:11434/api/generate', {2 method: 'POST',3 body: JSON.stringify({4 model: 'llama3.2:3b',5 prompt: longPrompt,6 keep_alive: '60m'7 })8});結果: 効果なし。keep_aliveはモデルのメモリ保持時間の設定であり、HTTPリクエストのタイムアウトとは無関係でした。
原因の特定:Node.js fetch APIのデフォルトタイムアウト
エラーコードUND_ERR_HEADERS_TIMEOUTを調査したところ、これはNode.jsのfetch実装(undici)が持つデフォルトのヘッダータイムアウトでした。
1Node.js fetch (undici)2 └── デフォルトのheadersTimeout: 300000ms(5分)3 └── OllamaのAPI応答が5分を超えると → HeadersTimeoutErrorOllama側の設定をいくら変更しても、HTTPクライアント側のタイムアウトが5分で切れていたのが原因です。
解決策
Node.js側のタイムアウト設定
fetchの代わりに、undiciのrequestを直接使うか、AbortControllerでタイムアウトを制御します。
1// AbortControllerによるタイムアウト制御2const controller = new AbortController();3const timeout = setTimeout(() => controller.abort(), 600000); // 10分4
5try {6 const response = await fetch('http://localhost:11434/api/generate', {7 method: 'POST',8 body: JSON.stringify({9 model: 'llama3.2:3b',10 prompt: longPrompt,11 stream: false12 }),13 signal: controller.signal14 });15 const data = await response.json();4 collapsed lines
16 return data.response;17} finally {18 clearTimeout(timeout);19}ストリーミングモードの活用
Ollamaのstream: trueオプションを使うと、レスポンスがチャンク単位で返されるため、ヘッダータイムアウトの問題を回避できます。
1const response = await fetch('http://localhost:11434/api/generate', {2 method: 'POST',3 body: JSON.stringify({4 model: 'llama3.2:3b',5 prompt: longPrompt,6 stream: true // ストリーミングモード7 })8});9
10let result = '';11const reader = response.body.getReader();12const decoder = new TextDecoder();13
14while (true) {15 const { done, value } = await reader.read();4 collapsed lines
16 if (done) break;17 const chunk = JSON.parse(decoder.decode(value));18 result += chunk.response;19}ストリーミングモードでは、最初のチャンクがすぐに返されるため、ヘッダータイムアウトに引っかかりません。
モデルサイズと精度のトレードオフ
処理速度より精度を優先
claudelogの用途では、要約の品質が重要です。高速だが不正確な要約よりも、時間がかかっても正確な要約を得たいという判断をしました。
モデル選定の考え方
1小さいモデル(llama3.2:3b)2 ├── メリット: 高速、メモリ消費少3 └── デメリット: 長文の要約精度が低い、文脈を見失いやすい4
5大きいモデル(※要確認: 実際に試したモデル名)6 ├── メリット: 要約精度が高い、長文の文脈保持が良い7 └── デメリット: 低速、メモリ消費大最終的には「プロンプトを詳細版に戻す + 大きなモデル」のオプションを選択しました。ローカルLLMの場合、モデルサイズの変更はollama pullで即座に行えるため、試行錯誤のコストは低いです。
プロンプトの最適化
モデルを大きくするだけでなく、プロンプト自体も調整しました。
1# 簡略プロンプト(3bモデルで使用していた)2「以下のログを要約してください。」3
4# 詳細プロンプト(大きなモデルで使用)5「以下はClaude Codeのセッションログです。6このログから以下の情報を抽出し、構造化された要約を生成してください。71. セッションの目的(何を実現しようとしていたか)82. 実施した主な作業(時系列順)93. 発生した問題とその解決策104. セッションの成果(作成/変更したファイル)115. 未完了のタスク(あれば)」詳細プロンプトの方が出力の品質は高くなりますが、トークン数が増えるため処理時間も長くなります。
ローカルLLM運用の実践知見
環境変数だけでは解決しない問題
今回の教訓は、エラーメッセージの発生箇所を正確に特定することの重要性です。
1エラー: HeadersTimeoutError (UND_ERR_HEADERS_TIMEOUT)2 ↓3直感: 「Ollamaのタイムアウト設定を変更すれば直る」4 ↓5実際: Node.jsのfetch実装(undici)のデフォルトタイムアウト6 ↓7解決: Node.js側のタイムアウト設定を変更UND_ERR_HEADERS_TIMEOUTのプレフィックスUNDはundiciを示しています。エラーコードの命名規則を知っていれば、より早く原因を特定できたかもしれません。
Ollamaの運用チェックリスト
ローカルLLMをアプリケーションに組み込む際のチェックリストです。
- HTTPクライアントのタイムアウト設定は十分か
- ストリーミングモードを使用しているか
- モデルサイズはタスクに対して適切か
- プロンプトの長さとモデルのコンテキストウィンドウの関係は適切か
- エラー発生時のリトライ処理はあるか
- モデルがメモリにロードされていない場合の初回遅延を考慮しているか
Node.js fetch APIの注意点
Node.jsのfetchはundiciベースの実装であり、ブラウザのfetchとは異なるデフォルト値を持ちます。
| 設定 | undiciのデフォルト | ブラウザfetch |
|---|---|---|
| headersTimeout | 300000ms (5分) | なし(無制限) |
| bodyTimeout | 300000ms (5分) | なし(無制限) |
| keepAliveTimeout | 4000ms | — |
ブラウザでは問題なく動作するコードが、Node.jsでタイムアウトする原因はここにあります。
まとめ
| 問題 | 原因 | 解決策 |
|---|---|---|
| HeadersTimeoutError | Node.js fetch (undici)のデフォルトタイムアウト | AbortControllerまたはストリーミングモード |
| OLLAMA_TIMEOUT無効 | タイムアウトの発生箇所がOllamaではない | Node.js側の設定変更 |
| 要約精度が低い | モデルサイズが小さすぎる | 大きなモデル + 詳細プロンプト |
ローカルLLMは「動かすだけ」は簡単ですが、アプリケーションに組み込んで安定運用するには、HTTPクライアントの挙動やモデル特性の理解が必要です。特にNode.js環境では、undici由来のデフォルト設定に注意してください。