Troubleshooting & Reliability Basics
10 examples to get you started with Troubleshooting & Reliability - 7 basic and 3 intermediate.
Prerequisites
- Install the official SDK:
pip install anthropic. - Set
ANTHROPIC_API_KEYin your environment before running any example. - Python's built-in
loggingmodule is used throughout; no extra logging library is required. - A basic familiarity with
try/exceptand theanthropicclient is assumed; nothing else is needed to follow along.
Basic Examples
1. Log Every Claude API Call
Wrap the client call so every request and response is logged before anything else happens.
import logging
import time
import anthropic
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("claude_client")
client = anthropic.Anthropic()
def call_claude(prompt: str) -> anthropic.types.Message:
started = time.monotonic()
logger.info("claude.request", extra={"prompt_len": len(prompt)})
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
elapsed_ms = (time.monotonic() - started) * 1000
logger.info(
"claude.response",
extra={"elapsed_ms": elapsed_ms, "stop_reason": response.stop_reason},
)
return response- Logging both the request and response gives you a paper trail before an error happens, not just after.
elapsed_msis the single most useful field for spotting latency creep before it becomes an outage.extra={}keeps structured fields separate from the log message so they can be queried later.- This wrapper is the seam every later example builds on.
Related: Understanding Why Claude Agents Fail in Production - the failure taxonomy this logging feeds into
2. Catch the Core SDK Exception Types
The anthropic SDK raises typed exceptions; catching the base class alone hides useful detail.
import anthropic
client = anthropic.Anthropic()
try:
client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[{"role": "user", "content": "Summarize this incident."}],
)
except anthropic.RateLimitError as e:
print("rate limited:", e.status_code)
except anthropic.APITimeoutError as e:
print("timed out:", e)
except anthropic.APIStatusError as e:
print("api error:", e.status_code, e.response)
except anthropic.APIConnectionError as e:
print("connection error:", e)RateLimitErroris a subclass ofAPIStatusError, so order matters: catch it before the more general class.APITimeoutErrorandAPIConnectionErrorare both transport-level, not API-level, failures.e.status_codeonAPIStatusErrortells you exactly which HTTP status came back.- Catching the base
Exceptionhere would hide which failure family you're dealing with.
3. Extract Request IDs for Support and RCAs
Every API response carries a request ID that Anthropic support (and your own RCA template) will ask for.
import anthropic
client = anthropic.Anthropic()
try:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[{"role": "user", "content": "Hello"}],
)
request_id = response._request_id
except anthropic.APIStatusError as e:
request_id = e.request_id
print(f"request {request_id} failed with status {e.status_code}")- The request ID is present on both successful responses and raised
APIStatusErrorexceptions. - Log it alongside every call, it is the fastest way to correlate a user complaint with a server-side trace.
- Without a captured request ID, you often cannot get further detail from a past failure after the fact.
- This single field turns "it broke earlier" into an actionable support ticket.
4. Classify an Error Into a Failure Family
Turn a raw exception into one of the failure families from the section's mental-model page.
import anthropic
def classify_error(exc: Exception) -> str:
if isinstance(exc, anthropic.RateLimitError):
return "capacity"
if isinstance(exc, (anthropic.APITimeoutError, anthropic.APIConnectionError)):
return "lifecycle"
if isinstance(exc, anthropic.APIStatusError) and exc.status_code >= 500:
return "capacity"
if isinstance(exc, anthropic.APIStatusError):
return "contract"
return "unknown"- Classifying at the point of capture means your logs and dashboards are queryable by failure family from day one.
- 5xx responses are grouped with capacity because they usually indicate transient server-side saturation.
- Any
APIStatusErrorthat isn't rate-limiting or 5xx is treated as a contract issue, like a bad request shape. - This function is intentionally small; extend it as new failure signatures show up in production.
Related: Diagnosing 429 Rate-Limit Errors and Quota Exhaustion Under Load - deep dive on the capacity family
5. Track Token Usage Per Call
Token counts are on every response and are your earliest signal of cost or context problems.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": "Explain vector clocks briefly."}],
)
usage = response.usage
print(f"input_tokens={usage.input_tokens} output_tokens={usage.output_tokens}")usage.input_tokensandusage.output_tokensare always present on a successful response.- Logging these per call lets you build a token-spend timeline without waiting for a monthly invoice.
- A sudden jump in
input_tokensfor the same logical request often signals prompt or context growth. - Cache-related usage fields exist too and matter once you're chasing cost spikes specifically.
Related: Prompt Cache Miss Storms: Diagnosing Sudden Cost and Latency Spikes - what to do when token usage spikes unexpectedly
6. Write a Minimal Health-Check Ping
A cheap, low-token call you can run on a schedule to confirm the API path is healthy end to end.
import anthropic
client = anthropic.Anthropic()
def health_check() -> bool:
try:
client.messages.create(
model="claude-haiku-4-5",
max_tokens=1,
messages=[{"role": "user", "content": "ping"}],
)
return True
except anthropic.APIError:
return False- Use the cheapest available model and a tiny
max_tokensto keep this call nearly free to run frequently. anthropic.APIErroris the shared base class for both status errors and connection errors, useful for a simple boolean check.- Running this every minute from your infrastructure gives you an independent signal from your real traffic's error rate.
- A failing health check plus a failing real-traffic error rate together confirm the problem is upstream, not local.
7. Log Tool-Use Blocks Before Parsing Them
When an agent uses tools, log the raw tool-use block before you attempt to parse its arguments.
import json
import logging
logger = logging.getLogger("claude_client")
def log_tool_use(response) -> None:
for block in response.content:
if block.type == "tool_use":
logger.info(
"claude.tool_use",
extra={"tool_name": block.name, "raw_input": json.dumps(block.input)},
)- Logging the raw tool-use block before parsing means you still have the evidence if parsing later fails.
block.inputfrom the SDK is already a parsed dict for well-formed calls, but capturing it as JSON keeps the log format consistent.- This is the first line of defense before the deeper repair logic covered in the malformed tool-use JSON article.
- Skipping this step is the most common reason a malformed tool call is undiagnosable after the fact.
Related: Handling Malformed Tool-Use JSON and Schema Validation Failures - catching and repairing bad tool-call output
Intermediate Examples
8. Combine Logging, Timing, and Classification
A single wrapper that ties the earlier pieces together into one diagnostic call site.
import logging
import time
import anthropic
logger = logging.getLogger("claude_client")
client = anthropic.Anthropic()
def diagnostic_call(prompt: str) -> anthropic.types.Message | None:
started = time.monotonic()
try:
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
except anthropic.APIError as e:
elapsed_ms = (time.monotonic() - started) * 1000
family = classify_error(e)
request_id = getattr(e, "request_id", None)
logger.error(
"claude.failure",
extra={"family": family, "elapsed_ms": elapsed_ms, "request_id": request_id},
)
return None
elapsed_ms = (time.monotonic() - started) * 1000
logger.info(
"claude.success",
extra={"elapsed_ms": elapsed_ms, "tokens": response.usage.output_tokens},
)
return response- This wrapper is the shape most production call sites converge on: time it, try it, classify failures, log either outcome.
classify_error(example 4) is reused here rather than duplicated, keeping the taxonomy consistent across the codebase.- Returning
Noneon failure, rather than re-raising, is a deliberate choice for call sites that can degrade gracefully. - Swap the
return Nonefor a re-raise if the caller needs to fail loudly instead.
9. Accumulate a Minimal Error-Rate Counter
Track a rolling error rate in memory as a first monitoring signal, before you wire up a real metrics backend.
from collections import deque
import time
class RollingErrorRate:
def __init__(self, window_seconds: int = 300):
self.window_seconds = window_seconds
self.events: deque[tuple[float, bool]] = deque()
def record(self, success: bool) -> None:
now = time.monotonic()
self.events.append((now, success))
cutoff = now - self.window_seconds
while self.events and self.events[0][0] < cutoff:
self.events.popleft()
def error_rate(self) -> float:
if not self.events:
return 0.0
failures = sum(1 for _, success in self.events if not success)
return failures / len(self.events)- A rolling window in memory is enough to catch a spike in a single process before you've integrated a real metrics stack.
record(success)is called from the diagnostic wrapper in example 8, once per call.- This is deliberately not a replacement for real observability tooling, only a bridge for the first version of an app.
- Watch
error_rate()cross a threshold (e.g. 5%) as your first automated signal that something changed.
10. Replay a Failing Request With Verbose Logging
A minimal script for the first step of any incident: reproduce the failure with full request and response visibility.
import logging
import anthropic
logging.basicConfig(level=logging.DEBUG)
client = anthropic.Anthropic()
def replay(prompt: str, model: str = "claude-sonnet-5") -> None:
try:
response = client.messages.create(
model=model,
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)
print("SUCCESS:", response.stop_reason, response.usage)
except anthropic.APIStatusError as e:
print("FAILED:", e.status_code, e.request_id, e.response.text)
if __name__ == "__main__":
replay("The prompt that was reported as failing")- Setting
logging.DEBUGon the SDK's own logger surfaces the raw HTTP request and response, which is often the fastest way to spot a malformed header or payload. - Running this as a standalone script, outside your normal application, isolates whether the failure is environmental or code-specific.
- This is the first concrete step in the diagnostic workflow that the rest of this section builds on.
- Once you can reliably reproduce a failure this way, you're ready to move to the specific failure-family article that matches it.
Related: Retry and Exponential Backoff Strategies for Transient Claude API Failures - what to build once you can reliably reproduce a transient failure Related: Troubleshooting & Reliability Best Practices - the checklist to apply once your basics are in place
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the official
anthropicPython SDK (latest 0.x release). Model names, pricing, and SDK versions move quickly - verify current specifics at platform.claude.com/docs before relying on them.