import { Callout } from '@/components/ui/callout';
If you have built anything against the Claude API for more than a few weeks, you have probably seen this:
Error: spanner temporarily unavailable
The message is short, the HTTP status varies between 503 and 500 depending on the path, and the same prompt usually succeeds twenty seconds later. That makes it hard to reproduce and even harder to decide whether the bug is on your side or Anthropic's.
The wording itself tells us something about Anthropic's infrastructure, and that reading points straight at the retry patterns that have actually held up in production for me. None of this is in the official docs; it is the kind of knowledge you accumulate by shipping real systems on top of Claude.
What "Spanner" Tells Us
As soon as the word "Spanner" appears, you can infer something about the architecture. Spanner is Google Cloud's globally distributed database, which strongly suggests parts of Anthropic's stack are deployed on GCP and rely on Spanner for metadata storage. In other words, this error is almost never about the model itself failing. It is about the metadata layer next to the model briefly stalling.
In practice, you tend to see this error during:
- saving conversation history or session state
- API key validation or usage metering
- reading attachments through Projects or Files APIs
- looking up billing-related data
Knowing this changes how you respond: the retry pattern that works for "model is overloaded" is different from the one that works for "metadata layer is briefly stalled."
First Triage: Is It You or Is It Them?
Before changing any code, do these three checks.
First, open Anthropic Status. Spanner-flavored issues often show up as a "Partial outage" and tend to recover within thirty to sixty minutes.
Second, capture the request-id header from the failing call. Without that ID, support cannot meaningfully investigate. I make it a habit to log the request ID alongside any non-200 response.
Third, audit your own retry loop. Spanner errors are transient, so a simple, well-behaved backoff usually resolves them on its own. The trap is the opposite: an unbounded retry loop will hammer the API and trip your account into a separate rate-limit error, which then masks the original cause.
A Retry Pattern That Actually Holds Up
Exponential backoff with jitter is the pattern I run in production. Spanner errors typically recover within seconds to tens of seconds, so starting with a small delay and growing it works well.
import anthropic
import time
import random
client = anthropic.Anthropic()
def call_claude_with_retry(messages, model="claude-sonnet-4-6", max_retries=5):
"""Wrapper that survives transient errors like 'spanner temporarily unavailable'."""
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:
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")Two details deserve attention. The first is that I match on the literal substring spanner. The SDK does not currently expose a typed exception specifically for this case, so reading the message body is the practical move. The second is the jitter: without it, multiple instances retrying in lockstep will create a secondary spike that can trigger another wave of failures.
When Backoff Alone Isn't Enough
If retries do not clear the error, or if you are seeing it intermittently for hours, work through these in order.
- Switch models. Sonnet 4.6 sometimes fails while Haiku 4.5 succeeds. The two appear to land on slightly different infrastructure segments.
- Switch regions or providers. If you have access to Vertex AI or AWS Bedrock, those alternative routes often clear when the direct API is degraded. For business-critical systems, having Bedrock as a fallback is the cheapest insurance you can buy.
- Reduce the request footprint. Temporarily lower your token count or attachment size per request. Smaller payloads put less pressure on the metadata layer and frequently start succeeding before larger ones do.
- Open a support ticket with the request ID. If you have done all of the above and the error persists, contact Anthropic support. Include the request ID, the timestamp in UTC, and the rough frequency. With those three, investigations move much faster.
How to Surface This in Your Product
If you are building something user-facing on top of Claude, you do not want to leak "spanner temporarily unavailable" to end users. On Claude Lab and the sister sites I run, the rule is simple:
- Retry internally, and tell users something gentle like "we're a little busy right now, please give it five seconds"
- If the retry still fails, show a generic "please try again" and put the technical detail in server logs
- For important form submissions, stash the user's input locally before retrying so a failed call does not destroy what they typed
You cannot drive the underlying error rate to zero, but you can drastically reduce the pain users feel.
Distinguishing Spanner Errors from Their Cousins
Finally, a quick taxonomy of related errors that look similar at a glance:
overloaded(429/529) — model-side congestion; backoff helps but capacity has to recover server-siderate_limit_exceeded(429) — your API key is over budget; fix usage, not retry logicinternal_server_error(500) — model-side fault; Anthropic owns this fullyspanner temporarily unavailable(503/500) — metadata layer stalling; the topic of this article
When several of these appear together, it is easy to mistake one for another. Logging both error.type and error.message makes the post-mortem much easier.
Next Step
If you have not yet, add exponential backoff with jitter to your Claude integration today. The perceived stability of your product will improve immediately, and you will stop chasing ghost incidents that a five-line wrapper would have absorbed.
Yes, this is on Anthropic's side
Let's lead with the conclusion. This error is an Anthropic-side (or, more precisely, an underlying Google Cloud) transient issue. It is not a bug in your code, your authentication, or your request format.
A typical response body looks like this:
{
"type": "error",
"error": {
"type": "api_error",
"message": "Spanner temporarily unavailable. Please retry after a brief delay."
}
}The HTTP status code is usually 503 Service Unavailable or 529 Overloaded — both of which signal "the server can't process this right now."
But I want to push back on the obvious instinct, which is to just retry blindly. I tried exactly that during my first incident and made things measurably worse, both for Anthropic and for myself.
Why a naive retry loop is dangerous
What makes this error sneaky is that it often happens at a scale too small to ever land on the Anthropic Status Page. The three incidents I've witnessed each lasted somewhere between 30 seconds and 5 minutes, in localized regions, with no public acknowledgment afterward.
If your code is running a tight while True: retry() during one of these moments, three bad things happen at once:
- You add load to a struggling backend — slowing recovery instead of helping it.
- Your own connection pool drains — healthy requests start queueing behind dead ones.
- Your token bill spikes — every retry that gets a non-cached response is billed.
In my first incident, I generated 8x my normal request volume in 5 minutes. I wasn't helping Anthropic, and I was paying for the privilege.
The three-tier retry pattern I now use
What I run today is a layered strategy. Python below, but the shape transfers cleanly to any language:
import time
import random
from anthropic import Anthropic, APIError, APIStatusError
client = Anthropic()
def call_claude_with_resilience(messages, model="claude-sonnet-4-6"):
"""Three-tier retry with circuit breaker behavior."""
# Tier 1: instant retry for transient blips
max_immediate_retries = 2
# Tier 2: exponential backoff for short outages
max_backoff_retries = 3
# Tier 3: fallback to a different model
fallback_model = "claude-haiku-4-5-20251001"
last_error = None
# Tier 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
# Tier 2: exponential backoff with jitter
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
# Tier 3: model fallback + alert
try:
notify_ops("Spanner error fallback triggered")
return client.messages.create(model=fallback_model, messages=messages, max_tokens=4096)
except APIStatusError:
# Real outage. Bubble the error up.
raise last_errorThree things matter about this shape:
Tier 1 is two fast retries with a 200–500ms delay. Most "spanner" errors I see are pure transient blips that clear within a single second. If you stop here, you've handled 90% of cases.
Tier 2 has exponential backoff with jitter. If multiple processes hit the same incident at once and all retry on the same schedule, you create a thundering-herd that prevents the backend from ever recovering. Jitter is non-negotiable.
Tier 3 is a model downgrade. I've personally observed cases where Sonnet was failing while Haiku stayed healthy on the same account. A slightly degraded answer is much better than a fully unavailable service from the user's perspective.
Batch jobs deserve a different philosophy
For interactive workloads (chatbots, in-app generation), the pattern above is what you want. For overnight batch jobs, I take a much more relaxed approach: fail fast, defer to tomorrow.
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:
# Push to the Anthropic Batch API or just retry tomorrow
enqueue_for_retry(failed, delay_hours=12)
log.info(f"Deferred {len(failed)} items due to API instability")For anything that genuinely tolerates a 24-hour SLA, the Anthropic Batch API absorbs these errors automatically and is what I default to now.
Build your own status page
The third lesson from my incidents: Anthropic's Status Page is too coarse to catch these. A 5-minute regional blip almost never makes the public log.
To compensate, I added a small set of metrics on my side:
- Claude API error rate in 5-minute windows, per model
- A counter for any response containing the string "spanner"
- A timeline of every fallback event (Tier 3 trigger)
The "spanner counter with a Slack alert" is the one that's earned its keep most. It catches incidents 10–30 minutes before any community Tweet, which gives me a head start on customer communication.
How to tell "outage" from "your design"
A short caveat. If you're seeing this error 5+ times per hour from the same API key, the probability shifts away from "Anthropic transient" and toward "your request shape is provoking it."
The two patterns I've seen cause this most often:
- Sending extremely long contexts (500K+ tokens) in a tight loop.
- A broken Prompt Caching setup that's silently sending the full prompt every call.
Logging the X-Cache-Hit header and watching cache hit rates is the cheapest way to catch the second one. If your hit rate is dramatically lower than expected, you've probably broken your cache key inadvertently.
The next time you see this error, start with Tier 1: two fast retries with jitter. If the error keeps showing up, invest a day in implementing the full three-tier pattern. The "I sleep through the night" benefit pays for the implementation cost many times over.