Skip to main content

Error Handling Guide

Use this to guide you in error handling for your application to gracefully handle API failures, rate limits, and network issues.

HTTP Status Codes

CodeErrorMeaningAction
401UnauthorizedInvalid API keyVerify key, check revocation
402Payment RequiredCU pool exhaustedEnable overage or upgrade tier
403ForbiddenInsufficient permissionsCheck API key scopes
429Too Many RequestsRate limit exceeded (CUPs)Implement backoff
500Internal Server ErrorServer-side issueRetry with backoff
503Service UnavailableMaintenance or overloadCheck status page, retry

Rate Limit Errors (429)

Understanding 429 Errors

You'll receive a 429 when you exceed your CUPs (Compute Units Per Second) limit:

{
"error": "Rate limit exceeded",
"message": "Throughput limit exceeded. Current: 15500 CUPs, Limit: 15000 CUPs",
"retry_after": 2,
"cups_limit": 15000,
"current_cups": 15500,
"tier": "Business"
}

Response Headers:

X-RateLimit-CUPs-Limit: 10000
X-RateLimit-CUPs-Remaining: 0
Retry-After: 2

Exponential Backoff (Python)

import time
import random
from typing import Callable, TypeVar, Any

T = TypeVar('T')

def exponential_backoff(
func: Callable[[], T],
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0
) -> T:
"""
Retry function with exponential backoff.

Args:
func: Function to retry
max_retries: Maximum retry attempts
base_delay: Initial delay in seconds
max_delay: Maximum delay cap

Returns:
Function result

Raises:
Last exception if all retries fail
"""
for attempt in range(max_retries):
try:
return func()
except RateLimitError as e:
if attempt == max_retries - 1:
raise

# Exponential backoff: 1s, 2s, 4s, 8s, 16s
delay = min(base_delay * (2 ** attempt), max_delay)

# Add jitter to prevent thundering herd
jitter = random.uniform(0, 0.1 * delay)
total_delay = delay + jitter

print(f"Rate limited. Retry {attempt + 1}/{max_retries} after {total_delay:.2f}s")
time.sleep(total_delay)

raise Exception("Max retries exceeded")

# Usage
from axol import AxolClient

client = AxolClient(api_key="your_key")

def get_block():
return client.get_block("ethereum", "latest")

block = exponential_backoff(get_block)

Exponential Backoff (JavaScript/TypeScript)

interface RetryOptions {
maxRetries?: number;
baseDelay?: number;
maxDelay?: number;
}

async function exponentialBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxRetries = 5,
baseDelay = 1000, // milliseconds
maxDelay = 60000
} = options;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (error.status !== 429 || attempt === maxRetries - 1) {
throw error;
}

// Exponential backoff with jitter
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * 0.1 * delay;
const totalDelay = delay + jitter;

console.log(`Rate limited. Retry ${attempt + 1}/${maxRetries} after ${totalDelay}ms`);
await new Promise(resolve => setTimeout(resolve, totalDelay));
}
}

throw new Error('Max retries exceeded');
}

// Usage
import { AxolClient } from '@axol/api-client';

const client = new AxolClient({ apiKey: 'your_key' });

const block = await exponentialBackoff(
() => client.getBlock('ethereum', 'latest')
);

Rate Limiting Strategy

Proactive approach - Track your CUPs usage:

class RateLimiter:
def __init__(self, cups_limit: int):
self.cups_limit = cups_limit
self.window_start = time.time()
self.cups_consumed = 0

def can_request(self, cu_cost: int) -> bool:
"""Check if request fits within rate limit"""
now = time.time()

# Reset window every second
if now - self.window_start >= 1.0:
self.window_start = now
self.cups_consumed = 0

return self.cups_consumed + cu_cost <= self.cups_limit

def consume(self, cu_cost: int):
"""Record CU consumption"""
self.cups_consumed += cu_cost

def wait_if_needed(self, cu_cost: int):
"""Sleep if request would exceed limit"""
if not self.can_request(cu_cost):
wait_time = 1.0 - (time.time() - self.window_start)
if wait_time > 0:
time.sleep(wait_time)
self.window_start = time.time()
self.cups_consumed = 0

# Usage
limiter = RateLimiter(cups_limit=15000) # Business tier

for _ in range(1000):
limiter.wait_if_needed(cu_cost=26) # eth_call cost
result = client.eth_call(...)
limiter.consume(26)

CU Pool Exhausted (402)

Understanding 402 Errors

Triggered when you've consumed your monthly CU allocation:

{
"error": "CU pool exhausted",
"message": "Monthly standard CU pool exhausted. 600M/600M CUs used.",
"cu_limit": 900000000,
"cu_used": 900000000,
"cu_remaining": 0,
"reset_date": "2025-11-10T00:00:00Z",
"tier": "Professional",
"options": {
"enable_overage": true,
"overage_rate": 0.50,
"upgrade_tier": "Business"
}
}

Decision Tree

def handle_cu_exhausted(error_response):
"""
Handle 402 CU pool exhausted error

Options:
1. Enable overage billing (costs extra)
2. Upgrade to higher tier (more CUs)
3. Wait until reset (free)
4. Optimize usage (reduce CU consumption)
"""
cu_used = error_response['cu_used']
reset_date = error_response['reset_date']
overage_rate = error_response['options']['overage_rate']

# Option 1: How much would overage cost?
additional_cus_needed = estimate_remaining_month_usage()
overage_cost = (additional_cus_needed / 1_000_000) * overage_rate

print(f"Overage billing: ${overage_cost:.2f} for {additional_cus_needed:,} CUs")

# Option 2: Next tier pricing
next_tier = error_response['options']['upgrade_tier']
print(f"Upgrade to {next_tier}: Check /docs/pricing")

# Option 3: Time until reset
import datetime
reset = datetime.fromisoformat(reset_date)
time_left = reset - datetime.now()
print(f"Reset in: {time_left.days} days, {time_left.seconds // 3600} hours")

# Option 4: Optimize usage
print("Optimization tips:")
print("- Cache responses for static data")
print("- Avoid trace methods when unnecessary")
print("- Use narrow block ranges for eth_getLogs")
print("- Batch requests when possible")

Graceful Degradation

class AxolClientWithFallback:
def __init__(self, primary_key, fallback_key=None):
self.primary = AxolClient(api_key=primary_key)
self.fallback = AxolClient(api_key=fallback_key) if fallback_key else None
self.using_fallback = False

def get_block(self, chain, block_number):
try:
if not self.using_fallback:
return self.primary.get_block(chain, block_number)
except CUPoolExhaustedError:
if self.fallback:
print("Primary CU pool exhausted, switching to fallback")
self.using_fallback = True
return self.fallback.get_block(chain, block_number)
raise

return self.fallback.get_block(chain, block_number)

Authentication Errors (401, 403)

401 Unauthorized

try:
client = AxolClient(api_key="your_key")
result = client.get_block("ethereum", "latest")
except AuthenticationError as e:
# Check common issues
print("Authentication failed:")
print("1. Verify API key is correct (check for spaces)")
print("2. Check if key was revoked at app.axol.io")
print("3. Ensure environment variable is set correctly")

# Log for debugging
import os
key = os.getenv('AXOL_API_KEY', '')
print(f"Key starts with: {key[:8]}...")
print(f"Key length: {len(key)} characters")

403 Forbidden

try:
result = client.submit_transaction(...)
except InsufficientPermissionsError as e:
print(f"API key lacks required scope: {e.required_scope}")
print("Solutions:")
print("1. Create new key with 'write:transactions' scope")
print("2. Use different key for write operations")
print("3. Contact support if scope should be available")

Server Errors (500, 503)

Retry Strategy

def is_retryable(status_code: int) -> bool:
"""Determine if error is worth retrying"""
return status_code in [429, 500, 502, 503, 504]

def should_retry(error, attempt: int, max_retries: int) -> bool:
"""Enhanced retry logic"""
if attempt >= max_retries:
return False

# Always retry rate limits and server errors
if hasattr(error, 'status_code'):
return is_retryable(error.status_code)

# Retry network errors
if isinstance(error, (ConnectionError, TimeoutError)):
return True

return False

# Usage
for attempt in range(5):
try:
result = client.get_block("ethereum", "latest")
break
except Exception as e:
if should_retry(e, attempt, max_retries=5):
wait = min(2 ** attempt, 32)
print(f"Retry {attempt + 1}/5 after {wait}s: {e}")
time.sleep(wait)
else:
raise

Circuit Breaker Pattern

Prevent cascading failures when API is down:

from enum import Enum
import time

class CircuitState(Enum):
CLOSED = "closed" # Normal operation
OPEN = "open" # Failing, reject requests
HALF_OPEN = "half_open" # Testing if recovered

class CircuitBreaker:
def __init__(
self,
failure_threshold: int = 5,
timeout: float = 60.0,
recovery_timeout: float = 30.0
):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = None
self.state = CircuitState.CLOSED

def call(self, func, *args, **kwargs):
if self.state == CircuitState.OPEN:
# Check if we should try to recover
if time.time() - self.last_failure_time >= self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
else:
raise Exception("Circuit breaker is OPEN")

try:
result = func(*args, **kwargs)

# Success - reset if in half-open state
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.CLOSED
self.failure_count = 0

return result

except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()

# Trip breaker if threshold exceeded
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
print(f"Circuit breaker OPEN after {self.failure_count} failures")

raise

# Usage
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30.0)

for _ in range(10):
try:
result = breaker.call(client.get_block, "ethereum", "latest")
print(f"Success: {result}")
except Exception as e:
print(f"Failed: {e}")

time.sleep(1)

Best Practices

1. Always Check Response Headers

response = client.get_block("ethereum", "latest")

# Monitor CU usage
cu_remaining = response.headers.get('X-RateLimit-CU-Remaining')
cu_limit = response.headers.get('X-RateLimit-CU-Limit')
usage_percent = (1 - int(cu_remaining) / int(cu_limit)) * 100

if usage_percent > 80:
print(f"WARNING: {usage_percent:.1f}% of monthly CU pool used")

# Monitor throughput
cups_remaining = response.headers.get('X-RateLimit-CUPs-Remaining')
if int(cups_remaining) < 1000:
print("WARNING: Approaching CUPs limit, slow down requests")

2. Implement Timeouts

# Set appropriate timeouts
client = AxolClient(
api_key="your_key",
timeout=30 # Default timeout
)

# Use higher timeout for trace methods
response = client.debug_trace_transaction(
tx_hash="0x...",
timeout=60 # Trace methods can take 30+ seconds
)

3. Cache Aggressively

from functools import lru_cache
from datetime import datetime, timedelta

class CachingClient:
def __init__(self, api_key):
self.client = AxolClient(api_key=api_key)
self._cache = {}

def get_block(self, chain, block_number):
# Only cache historical blocks (not 'latest')
if block_number == 'latest':
return self.client.get_block(chain, block_number)

cache_key = f"{chain}:{block_number}"

if cache_key in self._cache:
return self._cache[cache_key]

# Fetch and cache
block = self.client.get_block(chain, block_number)
self._cache[cache_key] = block
return block

4. Monitor and Alert

import logging

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('axol_client')

class MonitoredClient:
def __init__(self, api_key):
self.client = AxolClient(api_key=api_key)
self.error_count = 0
self.total_requests = 0

def get_block(self, chain, block_number):
self.total_requests += 1

try:
result = self.client.get_block(chain, block_number)

# Log CU usage
cu_used = result.headers.get('X-Request-CU-Cost', 0)
logger.info(f"Block request: {cu_used} CUs")

return result

except Exception as e:
self.error_count += 1
error_rate = self.error_count / self.total_requests

logger.error(f"Error: {e}")

# Alert if error rate too high
if error_rate > 0.1: # 10% error rate
logger.critical(f"High error rate: {error_rate:.1%}")

raise

Error Response Examples

Complete Error Structure

{
"error": "rate_limit_exceeded",
"message": "Throughput limit exceeded",
"status": 429,
"request_id": "req_abc123",
"timestamp": "2025-11-10T15:30:00Z",
"details": {
"cups_limit": 10000,
"cups_used": 10500,
"retry_after": 2
},
"documentation": "https://docs.axol.io/docs/concepts/rate-limits"
}

Handling in Code

try:
result = client.get_block("ethereum", "latest")
except AxolAPIError as e:
print(f"Error: {e.error}")
print(f"Message: {e.message}")
print(f"Request ID: {e.request_id}") # Include in support tickets
print(f"Docs: {e.documentation}")

if hasattr(e, 'retry_after'):
print(f"Retry after {e.retry_after} seconds")

Summary

Key Takeaways:

  1. Implement exponential backoff for 429 errors
  2. Monitor CU usage proactively (80%/90%/100% alerts)
  3. Cache historical data - blocks never change
  4. Use circuit breakers for cascading failure prevention
  5. Log request IDs for support tickets
  6. Set appropriate timeouts - trace methods need 60s+
  7. Handle 402 gracefully - enable overage or wait for reset

See also: