Making your first Claude API request? Hitting errors like authentication failures, rate limiting, or invalid requests? This guide covers the 9 most common issues beginners face and how to fix each one.
Q1: 401 Authentication Error or "API key not recognized"
Root Causes
API key is missing, invalid, incorrect format, or not set as environment variable.
Solutions
Step 1: Get your API key
- Visit Anthropic Console
- Log in to your account
- Generate a new API key (or copy existing one)
- Key starts with
sk-ant-
Step 2: Set the API key in your code
Python:
import anthropic
# Method 1: Environment variable (Recommended)
# First: export ANTHROPIC_API_KEY="sk-ant-..."
client = anthropic.Anthropic() # Reads ANTHROPIC_API_KEY automatically
# Method 2: Explicit parameter (for testing only)
client = anthropic.Anthropic(api_key="sk-ant-your-key-here")
# Verify (only shows first characters)
print(client.api_key[:10])JavaScript/Node.js:
import Anthropic from "@anthropic-ai/sdk";
// Method 1: Environment variable (Recommended)
// First: export ANTHROPIC_API_KEY="sk-ant-..."
const client = new Anthropic(); // Reads ANTHROPIC_API_KEY automatically
// Method 2: Explicit parameter
const client = new Anthropic({
apiKey: "sk-ant-your-key-here",
});cURL:
# Include API key in header
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: sk-ant-your-key-here" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{...}'Step 3: Set environment variable
# For Bash/Zsh
export ANTHROPIC_API_KEY="sk-ant-your-key-here"
# Make it permanent (add to ~/.bashrc or ~/.zshrc)
echo 'export ANTHROPIC_API_KEY="sk-ant-your-key-here"' >> ~/.bashrc
source ~/.bashrc
# Verify it's set
echo $ANTHROPIC_API_KEYStep 4: Test the key
# Test with curl
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model":"claude-opus-4-1",
"messages":[{"role":"user","content":"test"}],
"max_tokens":100
}' | head -c 100
# Should return API response (not an error):::warning If your API key is leaked:
- Delete the compromised key in Anthropic Console
- Generate a new one
- Update all references in your code
- Check git history for accidentally committed keys
Contact Anthropic support if fraudulent usage is detected on your account. :::
Q2: 429 Error (Too Many Requests) - Rate limiting
Root Causes
Exceeded API usage limits, sending too many requests simultaneously, or hitting quota limits.
Solutions
Step 1: Check rate limit details
# View rate limit info in response headers
curl -i https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{...}'
# Response includes:
# x-ratelimit-limit-requests: 6000
# x-ratelimit-remaining-requests: 5999
# x-ratelimit-reset-requests: 2026-03-21T18:05:00ZStep 2: Implement exponential backoff retry logic
Python:
import anthropic
import time
def call_claude_with_retry(client, max_retries=5, backoff_factor=2.0):
"""Retry API call with exponential backoff"""
for attempt in range(max_retries):
try:
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude!"}]
)
return response.content[0].text
except anthropic.RateLimitError:
if attempt == max_retries - 1:
raise # Give up on last attempt
# Exponential backoff: 1s, 2s, 4s, 8s, 16s
wait_time = backoff_factor ** attempt
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
# Usage
client = anthropic.Anthropic()
result = call_claude_with_retry(client)
print(result)Node.js:
import Anthropic from "@anthropic-ai/sdk";
async function callClaudeWithRetry(client, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await client.messages.create({
model: "claude-opus-4-1",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude!" }],
});
return response.content[0].text;
} catch (error) {
if (error.status === 429) {
if (attempt === maxRetries - 1) throw error;
// Exponential backoff in milliseconds
const waitMs = Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Waiting ${waitMs}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
} else {
throw error;
}
}
}
}
const client = new Anthropic();
const result = await callClaudeWithRetry(client);
console.log(result);Step 3: Limit concurrent requests
# ❌ Bad: Too many concurrent requests
import asyncio
async def bad_example():
tasks = [
client.messages.create(
model="claude-opus-4-1",
max_tokens=100,
messages=[{"role": "user", "content": f"Task {i}"}]
)
for i in range(100) # All 100 at once!
]
return await asyncio.gather(*tasks)
# ✅ Good: Limit concurrency to 5
from asyncio import Semaphore
async def good_example():
semaphore = Semaphore(5) # Max 5 concurrent
async def limited_request(i):
async with semaphore:
return await client.messages.create(
model="claude-opus-4-1",
max_tokens=100,
messages=[{"role": "user", "content": f"Task {i}"}]
)
tasks = [limited_request(i) for i in range(100)]
return await asyncio.gather(*tasks):::tip Rate limits differ by plan:
- Free tier: 6,000 requests/minute, ~2M tokens/day
- Paid tier: Varies by plan
Upgrade to a paid plan if you exceed free tier limits. :::
Q3: 400 Error (Bad Request) - Invalid request format
Root Causes
Wrong model name, missing required fields, invalid parameter values, or malformed JSON.
Solutions
Step 1: Verify model name
# ❌ Wrong (missing version)
response = client.messages.create(
model="claude-opus",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
)
# ✅ Correct model names
response = client.messages.create(
model="claude-opus-4-1", # Latest Opus
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
)Available models:
- claude-opus-4-1 (highest capability)
- claude-sonnet-4-20250514 (balanced)
- claude-haiku-4-5-20251001 (fast, lightweight)
Step 2: Include all required fields
# ✅ Required fields
response = client.messages.create(
model="claude-opus-4-1", # Required
max_tokens=1024, # Required (1-200000)
messages=[ # Required
{"role": "user", "content": "Hello"}
]
)
# ❌ Missing required max_tokens
response = client.messages.create(
model="claude-opus-4-1",
messages=[{"role": "user", "content": "Hello"}]
# Error: max_tokens is required
)Step 3: Validate parameter values
# ❌ Invalid parameter values
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=0, # ❌ Must be ≥ 1
temperature=-0.5, # ❌ Must be 0.0-1.0
top_p=1.5, # ❌ Must be 0.0-1.0
messages=[{"role": "user", "content": "Hello"}]
)
# ✅ Valid parameter values
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024, # 1-200000
temperature=0.7, # 0.0-1.0
top_p=0.9, # 0.0-1.0
messages=[{"role": "user", "content": "Hello"}]
)Step 4: Check message format
# ❌ Wrong format (string instead of dict)
messages = [
"Hello, Claude!" # ❌ Must be a dict
]
# ✅ Correct format
messages = [
{
"role": "user", # "user" or "assistant"
"content": "Hello, Claude!"
}
]
# ✅ Complex content (text + image)
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What's in this image?"
},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "base64-encoded-image"
}
}
]
}
]Step 5: Debug the error
import anthropic
try:
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
)
except anthropic.BadRequestError as e:
print(f"Error type: {e.error}")
print(f"Error message: {e.message}")
# Error message contains specific details about what was wrong:::warning Check API reference docs for:
- Valid model names (they change as new versions release)
- Acceptable parameter ranges
- Required vs optional fields :::
Q4: Empty response or max_tokens too small
Root Causes
max_tokens set too low (response gets truncated), or API call incomplete.
Solutions
Step 1: Set appropriate max_tokens
# ❌ max_tokens too small
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1, # ❌ Only 1 token of output!
messages=[{"role": "user", "content": "Write a 500-word essay"}]
)
# Result: truncated or empty response
# ✅ Reasonable max_tokens
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=2048, # Typical value
messages=[{"role": "user", "content": "Write an essay"}]
)Step 2: Check if response is complete
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
)
# Check completion status
if response.stop_reason == "end_turn":
print("✓ Response is complete")
elif response.stop_reason == "max_tokens":
print("❌ Response was truncated (increase max_tokens)")
else:
print(f"Stop reason: {response.stop_reason}")Step 3: Verify response content
# Check response isn't empty
if response.content:
text = response.content[0].text
print(f"Response length: {len(text)} characters")
else:
print("❌ Response is empty")
# View full response structure
print(f"Stop reason: {response.stop_reason}")
print(f"Usage: input={response.usage.input_tokens}, output={response.usage.output_tokens}")
print(f"Content blocks: {len(response.content)}")Step 4: Verify your prompt
# ❌ Vague prompt might get empty response
messages = [{"role": "user", "content": ""}]
# ✅ Clear, specific prompt
messages = [{
"role": "user",
"content": "Explain this Python code: [code snippet here]"
}]:::tip Context window limits by model:
- Claude Opus 4.1: 200,000 tokens
- Claude Sonnet 4: 200,000 tokens
- Claude Haiku: 200,000 tokens
Set max_tokens within context window limits. For typical tasks, 1024-4096 tokens is usually sufficient. :::
Q5: Streaming not working or partial response
Root Causes
Incorrect streaming implementation, network disconnection, or incomplete request.
Solutions
Step 1: Basic streaming implementation
Python:
import anthropic
client = anthropic.Anthropic()
# ✅ Enable streaming
with client.messages.stream(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude!"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True) # flush() for real-time
print() # Newline at endNode.js:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = await client.messages.stream({
model: "claude-opus-4-1",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude!" }],
});
// Print streamed text in real-time
for await (const chunk of stream) {
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
process.stdout.write(chunk.delta.text);
}
}Step 2: Error handling for streams
try:
with client.messages.stream(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
) as stream:
full_response = ""
for text in stream.text_stream:
full_response += text
print(text, end="", flush=True)
# After streaming ends
final_message = stream.get_final_message()
print(f"\nStop reason: {final_message.stop_reason}")
except anthropic.APIError as e:
print(f"Stream error: {e}")Step 3: Set timeout for long streams
# Set timeout to avoid hanging indefinitely
client = anthropic.Anthropic(timeout=60.0) # 60 seconds
try:
with client.messages.stream(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
except anthropic.APITimeoutError:
print("Request timed out")Step 4: Check network connectivity
# Test connection to API
curl -I https://api.anthropic.com
# Should return 200 OK:::warning Streaming tips:
- Use flush() to display text in real-time
- Don't buffer responses in memory (defeats streaming purpose)
- Handle timeout errors gracefully
- Be aware of network interruptions :::
Q6: Tool use response format incorrect or unexpected
Root Causes
Malformed tool definition, incorrect tool_choice setting, or incomplete tool schema.
Solutions
Step 1: Define tools correctly
# ✅ Correct tool definition
tools = [
{
"name": "calculator",
"description": "Performs mathematical operations",
"input_schema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The operation to perform"
},
"a": {
"type": "number",
"description": "First operand"
},
"b": {
"type": "number",
"description": "Second operand"
}
},
"required": ["operation", "a", "b"]
}
}
]Step 2: Use tool in messages request
# ✅ Request with tools
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "What is 5 + 3?"}
]
)
# Check if tool was called
for block in response.content:
if block.type == "tool_use":
print(f"Tool used: {block.name}")
print(f"Input: {block.input}")Step 3: Control tool usage with tool_choice
# Auto: Claude decides whether to use tools
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto"},
messages=[{"role": "user", "content": "Calculate something"}]
)
# Or force a specific tool
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
tools=tools,
tool_choice={
"type": "tool",
"name": "calculator" # Force this tool
},
messages=[{"role": "user", "content": "Calculate something"}]
)Step 4: Loop tool calls until completion
messages = [
{"role": "user", "content": "Calculate 100 + 200 + 300"}
]
while True:
# Get response
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
tools=tools,
messages=messages
)
# Add assistant response to history
messages.append({"role": "assistant", "content": response.content})
# Look for tool calls
tool_calls = [block for block in response.content if block.type == "tool_use"]
if not tool_calls:
# No tools called, done
break
# Execute tools and add results
for tool_call in tool_calls:
# Simulate tool execution
if tool_call.name == "calculator":
a, b = tool_call.input["a"], tool_call.input["b"]
result = a + b
# Add tool result
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": str(result)
}
]
})
# Final response in messages[-1]:::warning Tool schema requirements:
- Must be valid JSON Schema
- All required fields in "required" array
- Clear, detailed descriptions (Claude uses these for selection)
- Reasonable input constraints :::
Q7: Context window exceeded error
Root Causes
Total input size exceeds model's maximum token limit.
Solutions
Step 1: Catch the error
try:
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[
{"role": "user", "content": very_long_text}
]
)
except anthropic.BadRequestError as e:
if "context_length_exceeded" in str(e):
print("Context window exceeded! Split your input.")Step 2: Split large inputs
# ❌ Bad: Send entire large document at once
long_text = "..." * 100000 # Too large
# ✅ Good: Split and process in chunks
def summarize_large_text(text, chunk_size=10000):
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
summaries = []
for i, chunk in enumerate(chunks):
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=500,
messages=[{
"role": "user",
"content": f"Summarize section {i+1}/{len(chunks)}:\n{chunk}"
}]
)
summaries.append(response.content[0].text)
return summariesStep 3: Manage conversation history
# Keep recent messages, discard old ones
def trim_history(messages, max_messages=10):
"""Keep only recent messages within limit"""
system_msgs = [m for m in messages if m.get("role") == "system"]
other_msgs = [m for m in messages if m.get("role") != "system"]
if len(other_msgs) > max_messages:
# Keep most recent
other_msgs = other_msgs[-max_messages:]
return system_msgs + other_msgsStep 4: Monitor token usage
response = client.messages.create(
model="claude-opus-4-1",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}]
)
input_tokens = response.usage.input_tokens
output_tokens = response.usage.output_tokens
total = input_tokens + output_tokens
print(f"Input tokens: {input_tokens}")
print(f"Output tokens: {output_tokens}")
print(f"Total: {total}"):::tip Context window limits:
- Claude Opus 4.1: 200,000 tokens (~1.5M words)
- Claude Sonnet 4: 200,000 tokens
- Claude Haiku: 200,000 tokens
For large document processing, always split into multiple API calls. :::
Q8: "tool_result could not be submitted" stops the conversation
Cause
Almost always, your tool_use and tool_result blocks are out of sync. Common cases: the tool_use_id doesn't match, you send a tool_result without first appending the assistant's tool_use turn, or you return tool_results for only some of the tool_use blocks.
Claude strictly requires a one-to-one pairing between the tool_use blocks in the previous assistant turn and the tool_result blocks in the next user turn. Miss even one and the entire request is rejected.
Solution
Step 1: Append the assistant response as-is
response = client.messages.create(
model="claude-opus-4-1", max_tokens=1024, tools=tools, messages=messages
)
# Append the full response.content (including tool_use blocks)
messages.append({"role": "assistant", "content": response.content})Don't rebuild the tool_use blocks yourself. Passing back the returned response.content verbatim is the most robust approach.
Step 2: Return a tool_result for every tool_use
tool_uses = [b for b in response.content if b.type == "tool_use"]
tool_results = []
for tu in tool_uses:
try:
output = run_tool(tu.name, tu.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tu.id, # must match exactly
"content": str(output),
})
except Exception as e:
# Always return a tool_result, even on failure — set is_error
tool_results.append({
"type": "tool_result",
"tool_use_id": tu.id,
"content": f"Error: {e}",
"is_error": True,
})
# Return all results in a single user message
messages.append({"role": "user", "content": tool_results})If three tools were called, you need three tool_results. Sending fewer triggers this error.
:::warning Frequent mistakes:
- Typo in tool_use_id, or reusing an id from a different call
- Tool execution throws and you forget to append the tool_result (always return one with is_error)
- A text block inserted before the tool_result (the content array must start with tool_result) :::
Q9: "Spanner temporarily unavailable" appears out of nowhere
Cause
This is not a problem in your code. It's a transient server-side error indicating that Anthropic's backend (a distributed database layer) is momentarily unavailable. Like 529 (Overloaded) and other 5xx errors, it clears up on its own after a short wait.
Seeing this on your first integration can make you doubt your request — as an indie developer, I paused and checked my setup the first time too — but assuming the request format is valid, all you need is a solid retry strategy.
Solution
Step 1: Retry with exponential backoff, treating it as transient
import time, random
import anthropic
def create_with_retry(client, max_retries=5, **kwargs):
for attempt in range(max_retries):
try:
return client.messages.create(**kwargs)
except (anthropic.InternalServerError, anthropic.APIStatusError) as e:
msg = str(e).lower()
transient = "spanner" in msg or "overloaded" in msg or "temporarily" in msg
if not transient or attempt == max_retries - 1:
raise
# Exponential backoff + jitter to avoid synchronized retries
wait = min(2 ** attempt, 30) + random.uniform(0, 1)
time.sleep(wait)Step 2: Don't hammer the API Retrying rapidly before recovery only prolongs the congestion. Wait 1–2 seconds first, then double each time up to a cap.
:::tip Quick rule of thumb:
Spanner temporarily unavailable/Overloaded/ 500 / 529 → transient server-side; retry with backoff- 400 / 401 / 404 → your request is wrong; retrying won't help
If it persists, check the Anthropic status page for incidents. :::
Looking back
Most Claude API errors fall into these categories:
- Authentication → Verify API key is set correctly (Q1)
- Rate limiting → Implement exponential backoff and limit concurrency (Q2)
- Request format → Check model names, required fields, and parameter values (Q3)
- Empty/truncated responses → Increase max_tokens or fix your prompt (Q4)
- Streaming → Use proper stream syntax and error handling (Q5)
- Tool use → Validate tool schema and loop until completion (Q6)
- Large inputs → Split into smaller chunks and process sequentially (Q7)
- tool_result submission → Pair every tool_use with a matching tool_result (Q8)
- Transient server errors → Retry "Spanner temporarily unavailable" with backoff (Q9)
Save complete error messages and check the API error code. Most issues resolve by following these steps.
Happy building with Claude API!