import { Callout } from '@/components/ui/callout';
Claude API で開発をしていると、ある日突然レスポンスにこんなメッセージが返ってくることがあります。
Error: spanner temporarily unavailable
メッセージそのものは短く、コード(HTTP ステータス)も状況によって 503 だったり 500 だったりとブレが出ます。しかも、同じプロンプトを数十秒後に投げ直すと何事もなかったように成功するため、再現も取りにくく、「うちのコードの問題なのか、Anthropic 側の障害なのか」が判断しにくいエラーです。
Spanner という名前から推測できる Anthropic のインフラ構造に触れたうえで、開発者が現場で取れる実践的な対処パターンを実例とともに整理しました。「公式ドキュメントに書いていないが、実装してみると効く」レベルの粒度を意識しました。
「Spanner」とは何か — エラーメッセージから読み取れること
Spanner という単語が出てきた瞬間に、ある程度内部の構造が見えてきます。Spanner は Google Cloud が提供するグローバル分散データベースの名前です。Anthropic のインフラの一部が GCP の Spanner に依存していると考えるのが自然で、つまりこのエラーは「Claude のモデル推論サーバーそのものではなく、その手前または周辺にあるストレージレイヤが一時的に応答していない」状態を示しています。
具体的には、以下のような場面で出やすいことが観測されています。
- 会話履歴やセッション情報を保存している途中
- API キーの検証や使用量メータリングが走るとき
- Projects や Files API で添付物を読み出すとき
- 課金関連のデータを照会するとき
つまり「モデルが落ちている」のではなく、「モデルの周りにあるメタデータ系の処理が一時的にスローダウンしている」状況です。これが分かると、対処の方針も自然に決まってきます。
開発者がまず確認すべきこと
エラーが出たときに最初に切り分けたいのは「自分のコードの問題か、Anthropic 側の問題か」です。次の3点をチェックしてください。
第一に、Anthropic Status を開いて全体的な稼働状況を確認します。Spanner 系の障害は「Partial outage」として一覧に出ることが多く、長くても 30 分から 1 時間で復旧する性質のものです。
第二に、リクエスト ID(request-id ヘッダー)を控えておきます。サポートに問い合わせる際に、この ID が無いと調査がほぼ進みません。私はエラーログを書く時点で必ず request ID をセットで残すようにしています。
第三に、自分の側のリトライロジックが暴走していないか確認します。Spanner エラーは一時的なので、適切なバックオフを入れれば自然に解消することがほとんどです。逆に、即座にリトライを連打すると Anthropic 側のレートリミッターに引っかかって別のエラーが出るので、ここは意外と重要です。
推奨されるリトライ戦略 — 実装サンプル
私が実際にプロダクションで使っているのは、「指数バックオフ+ジッター」の組み合わせです。Spanner エラーは数秒〜数十秒で復旧するので、最初は短く、徐々に間隔を広げていくのが効率的です。
import anthropic
import time
import random
client = anthropic.Anthropic()
def call_claude_with_retry(messages, model="claude-sonnet-4-6", max_retries=5):
"""Spanner などの一時的なエラーに耐えるラッパー"""
for attempt in range(max_retries):
try:
response = client.messages.create(
model=model,
max_tokens=1024,
messages=messages,
)
return response
except anthropic.APIStatusError as e:
# spanner temporarily unavailable は 503 で返ってくる
error_text = str(e).lower()
is_transient = (
"spanner" in error_text
or "temporarily unavailable" in error_text
or e.status_code in (502, 503, 504)
)
if not is_transient or attempt == max_retries - 1:
raise
# 指数バックオフ + ジッター(連打を防ぐ)
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Transient error, retrying in {wait:.1f}s...")
time.sleep(wait)
raise RuntimeError("max retries exceeded")ポイントは2つあります。1つ目は「spanner という文字列を素直にチェックしている」こと。Anthropic SDK の例外型でこのエラーを直接拾うのは現状むずかしいので、メッセージ本文を見るのが現実的です。2つ目は「ジッターを入れる」こと。ジッターが無いと、複数のサーバーが同時にリトライして二次的なスパイクを生むことがあります。
それでもエラーが続く場合に効くこと
リトライしても収まらない、あるいは同じエラーが断続的に何時間も続く場合は、次の打ち手を順番に試します。
- モデルを切り替えてみる — Sonnet 4.6 が応答しないときに Haiku 4.5 が応答するケースが時々あります。モデルごとに使うインフラセグメントが微妙に違うようです。
- リージョンを変える — Vertex AI 経由や AWS Bedrock 経由で Claude を使っている場合は、リージョンを切り替えると回避できることがあります。逆に、direct API しか使っていない場合は、業務上重要なシステムには Bedrock などのフォールバック経路を用意しておくのが安心です。
- 問い合わせの粒度を下げる — 1リクエストあたりのトークン量や添付ファイル数を一時的に減らすと、Spanner にかかる負荷が下がって通ることがあります。長文プロンプトをまとめて投げるのではなく、小さな会話に分割する手段です。
- サポートに request ID 付きで連絡する — ここまでやっても解消しない場合は、Anthropic サポート に問い合わせます。request ID と発生時刻(UTC)、頻度を伝えると、調査が早く進みます。
自分のサービスでこのエラーをどう扱うか
ユーザー向けにアプリを公開している立場で考えると、Spanner エラーを「ユーザーにそのまま見せる」のは避けたいところです。私が運営している Claude Lab や姉妹サイトでも、内部で Claude API を呼ぶ部分は次のような UX 設計にしています。
- リトライは内部で済ませ、ユーザーには「いま少し混み合っています、5秒ほどお待ちください」のような柔らかい表現で伝える
- それでも失敗したときは「もう一度お試しください」のシンプルなメッセージにし、技術的な詳細はサーバーログに残す
- 重要なフォーム送信などは、エラー発生時に入力内容をローカルに退避してから再試行を促す
エラーそのものをゼロにすることはできませんが、「ユーザーが体験する痛みを最小化する」設計はコード側で十分にコントロールできます。
関連するエラーとの違い
最後に、似た顔をした他のエラーとの見分け方を整理しておきます。
overloaded(429/529) — モデル側が混雑しているサイン。バックオフは効くが、サーバー側の容量が回復するまで時間がかかることが多いrate_limit_exceeded(429) — こちらの API キーが使いすぎ。リトライではなく利用量の見直しで対処するinternal_server_error(500) — モデル本体の異常。Spanner エラーよりも頻度が高く、こちらは Anthropic 側で完全に対処すべき領域spanner temporarily unavailable(503/500) — メタデータ系のスローダウン。今回扱った内容
似たエラーが連続して出ているように見えるときも、実は別の原因が混ざっていることがあります。error.type と error.message を両方ログに残しておくと、後の分析がぐっと楽になります。
次のアクション
このエラーに当たったら、まずは指数バックオフ+ジッターのリトライをコードに入れることから始めてみてください。それだけで体感の安定感が大きく変わります。本番環境で Claude を運用する場合は、Bedrock など別経路をフォールバックとして用意しておくと、長時間障害にも耐えられます。
このエラーは Anthropic 側の問題です
最初に結論をお伝えしておきます。このエラーは原則として Anthropic(または Anthropic が乗っている Google Cloud)側の一時的な不調が原因 です。あなたのコード、認証、リクエスト形式に問題があるわけではありません。
レスポンスの典型例は次のような構造です。
{
"type": "error",
"error": {
"type": "api_error",
"message": "Spanner temporarily unavailable. Please retry after a brief delay."
}
}HTTP ステータスコードは多くの場合 503 Service Unavailable または 529 Overloaded で返ってきます。つまり「サーバー側で一時的に処理できない状態」を示すレスポンスです。
ただし、これを 「リトライすればいいだけ」と片付けてしまうのは危険 だと、私は本番運用で痛感しました。理由は次の章に書きます。
なぜ「単純リトライ」では危険なのか
このエラーが厄介なのは、Anthropic のステータスページに反映されないレベルの「ミニ障害」 として発生することがある点です。私が観測した 3 回のケースでは、いずれも 30 秒〜5 分程度の局所的な不調で、Anthropic の Status Page には何も載りませんでした。
このとき、何も考えずに while True: retry() のような実装をしていると、以下が同時に起きます。
- Anthropic 側の負荷をさらに押し上げる ── 復旧を遅らせる加害者になる
- 自分のサーバーのコネクションプールが枯渇する ── 他の正常リクエストまで詰まる
- API 利用料金が瞬間的に跳ね上がる ── リトライ分のトークンも課金される
私は最初の障害でこれをやってしまい、5 分間で通常の 8 倍のリクエスト数を発生させました。Anthropic 側に貢献していないだけでなく、自分のクラウド請求書も無駄に膨らむ結果になりました。
推奨するリトライ戦略:3層構造
私が現在運用している実装は、3 層に分けたエラーハンドリング です。Python での例を示しますが、考え方はどの言語でも同じです。
import time
import random
from anthropic import Anthropic, APIError, APIStatusError
client = Anthropic()
def call_claude_with_resilience(messages, model="claude-sonnet-4-6"):
"""3層リトライ + サーキットブレーカー対応"""
# 第1層: 即座のリトライ(瞬間的な揺らぎ対策)
max_immediate_retries = 2
# 第2層: 指数バックオフ(短期の不調対策)
max_backoff_retries = 3
# 第3層: フォールバック(長期障害対策)
fallback_model = "claude-haiku-4-5-20251001"
last_error = None
# 第1層
for attempt in range(max_immediate_retries):
try:
return client.messages.create(model=model, messages=messages, max_tokens=4096)
except APIStatusError as e:
if e.status_code in (503, 529) and "spanner" in str(e).lower():
last_error = e
time.sleep(0.2 + random.random() * 0.3)
continue
raise
# 第2層: 指数バックオフ
for attempt in range(max_backoff_retries):
delay = (2 ** attempt) + random.random()
time.sleep(delay)
try:
return client.messages.create(model=model, messages=messages, max_tokens=4096)
except APIStatusError as e:
if e.status_code in (503, 529):
last_error = e
continue
raise
# 第3層: モデル降格 + 通知
try:
notify_ops("Spanner error fallback triggered")
return client.messages.create(model=fallback_model, messages=messages, max_tokens=4096)
except APIStatusError:
# ここまで来たら本格障害。呼び出し元に責任を渡す
raise last_errorポイントは 3 つあります。
ひとつめは、第 1 層で「200ms 程度の瞬間リトライを 2 回」だけ入れていることです。本当に瞬間的な揺らぎであれば、これだけで 9 割は解決します。
ふたつめは、第 2 層で 必ず指数バックオフ + ジッター を入れていることです。これがないと、複数のサーバープロセスが同じタイミングでリトライを叩き続け、サーバー側の復旧を妨げる「サンダリングハード」現象が起きます。
みっつめは、第 3 層で モデル降格 を選択肢として持っていることです。Sonnet が落ちていても Haiku は健全、というケースを実際に観測しました。応答品質が多少下がっても、ユーザーに「サービスが完全停止している」と感じさせないことの方が重要、というのが私の判断です。
バッチ処理での対応はもう一段別
リアルタイムな対話インターフェースではなく、夜間バッチで Claude を回しているケースでは、リトライ戦略の哲学が変わります。
私はバッチ処理では 「失敗したリクエストを別キューに退避し、翌日まとめて再処理する」 という割り切った設計にしています。理由は、夜間バッチの瞬間的な遅延はビジネス的にほぼ無害だからです。
def process_batch(items):
failed = []
for item in items:
try:
result = call_claude_with_resilience(item.messages)
save(item, result)
except APIStatusError as e:
if e.status_code in (503, 529):
failed.append(item)
else:
raise
if failed:
# Anthropic Batch API に投げ直す or 翌日キューに退避
enqueue_for_retry(failed, delay_hours=12)
log.info(f"Deferred {len(failed)} items due to API instability")Anthropic Batch API を併用すると、もともと 24 時間以内の処理が前提なので、Spanner エラーのような一時的な不調があっても自動的に吸収されます。私はバッチ用途ではこちらを優先するようになりました。
ステータスページに頼り切らない監視を仕込む
3 回の障害を経験して学んだもうひとつの教訓は、Anthropic の Status Page は粒度が粗い ということです。1 リージョンの 5 分間の不調はほぼ載りません。
そこで、私は自分のアプリケーション側に以下のメトリクスを仕込みました。
- Claude API 呼び出しの エラー率(5 分窓 / モデル別)
- レスポンス本文に "spanner" が含まれた呼び出しのカウント
- フォールバック発動回数(モデル降格イベント)の 時系列グラフ
このうち「spanner カウント」が一定値を超えたら Slack に通知するシンプルな仕組みを入れているのですが、これが思いのほか役に立ちました。Anthropic が公式に何も発表していない段階で「いま不調が発生している」と把握でき、ユーザーへの初動対応が早くなります。
「不調」と「設計の限界」の見分け方
最後に、頻発するようになった場合の見分け方をひとつだけ書いておきます。
このエラーが 同一 API キーから 1 時間以内に 5 回以上 発生する場合、Anthropic 側の不調ではなく 自分側のリクエストパターンに問題がある可能性 を疑うべきです。
具体的には、極端に長いコンテキスト(500K トークン超)を連続して投げる、Prompt Caching の設計が破綻していて毎回フルプロンプトを送っている、といったケースで、Spanner エラーを誘発しやすくなります。私の経験上、後者のパターンは結構多いです。
X-Cache-Hit ヘッダーを定期的にログに残し、キャッシュヒット率が想定より低いときはプロンプト構造を見直す、という運用が効きます。
このエラーに次に出会ったら、まずは 第 1 層の 200ms リトライを 2 回 だけ入れてみてください。それで解決しなければ、上で示した 3 層構造を 1 日かけて実装する価値があります。本番運用で「夜中に起きなくて済む安心感」が、実装にかける時間を上回ります。