Coinbase API Rate Limit and Error Handling: The Elegant Solution
Coinbase API Rate Limit and Error Handling: The Elegant Solution
Every developer who has built a trading bot on the Coinbase API has hit the dreaded 429 Too Many Requests. This article shows you how to handle it properly — and every other API error that will eventually bite you in production.
Understanding Coinbase Rate Limits
Coinbase Advanced Trade API enforces these limits:
- Public endpoints: 10 requests/second
- Private endpoints: 15 requests/second
- WebSocket connections: 750 messages/second per connection
The tricky part: these limits are per-IP, not per-API-key. If you're running multiple bots on the same server, they share the same budget.
The Retry Pattern That Actually Works
import time
import hmac
import hashlib
import requests
from typing import Optional, Any
from dataclasses import dataclass
@dataclass
class APIConfig:
api_key: str
api_secret: str
base_url: str = "https://api.coinbase.com"
max_retries: int = 5
base_delay: float = 0.5 # seconds
class CoinbaseClient:
def __init__(self, config: APIConfig):
self.config = config
self.session = requests.Session()
self._request_count = 0
def _sign_request(self, method: str, path: str, body: str = "") -> dict:
timestamp = str(int(time.time()))
message = timestamp + method.upper() + path + body
signature = hmac.new(
self.config.api_secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return {
"CB-ACCESS-KEY": self.config.api_key,
"CB-ACCESS-SIGN": signature,
"CB-ACCESS-TIMESTAMP": timestamp,
"Content-Type": "application/json"
}
def _request(
self,
method: str,
path: str,
body: Optional[dict] = None,
) -> Any:
"""Execute API request with exponential backoff retry."""
import json
body_str = json.dumps(body) if body else ""
url = f"{self.config.base_url}{path}"
for attempt in range(self.config.max_retries):
try:
headers = self._sign_request(method, path, body_str)
response = self.session.request(
method, url,
headers=headers,
data=body_str if body else None,
timeout=10
)
# Success
if response.status_code == 200:
self._request_count += 1
return response.json()
# Rate limited — back off
if response.status_code == 429:
retry_after = int(
response.headers.get("Retry-After", 1)
)
delay = max(
retry_after,
self.config.base_delay * (2 ** attempt)
)
print(f"Rate limited. Waiting {delay:.1f}s "
f"(attempt {attempt + 1})")
time.sleep(delay)
continue
# Server errors — retry
if response.status_code >= 500:
delay = self.config.base_delay * (2 ** attempt)
print(f"Server error {response.status_code}. "
f"Retrying in {delay:.1f}s")
time.sleep(delay)
continue
# Client errors — don't retry
response.raise_for_status()
except requests.exceptions.ConnectionError:
delay = self.config.base_delay * (2 ** attempt)
print(f"Connection error. Retrying in {delay:.1f}s")
time.sleep(delay)
except requests.exceptions.Timeout:
delay = self.config.base_delay * (2 ** attempt)
print(f"Timeout. Retrying in {delay:.1f}s")
time.sleep(delay)
raise Exception(
f"Max retries ({self.config.max_retries}) exceeded for {path}"
)
def get_accounts(self):
return self._request("GET", "/api/v3/brokerage/accounts")
def get_product(self, product_id: str):
return self._request(
"GET", f"/api/v3/brokerage/products/{product_id}"
)
def place_order(self, product_id: str, side: str,
size: str, price: Optional[str] = None):
import uuid
body = {
"client_order_id": str(uuid.uuid4()),
"product_id": product_id,
"side": side,
"order_configuration": {
"market_market_ioc": {"quote_size": size}
} if not price else {
"limit_limit_gtc": {
"base_size": size,
"limit_price": price
}
}
}
return self._request("POST", "/api/v3/brokerage/orders", body)
Pre-Emptive Rate Limiting
Instead of waiting to hit the limit, throttle proactively:
import asyncio
from collections import deque
class RateLimiter:
def __init__(self, max_requests: int = 10, window: float = 1.0):
self.max_requests = max_requests
self.window = window
self.timestamps: deque = deque()
async def acquire(self):
now = time.time()
# Remove timestamps outside the window
while self.timestamps and self.timestamps[0] < now - self.window:
self.timestamps.popleft()
if len(self.timestamps) >= self.max_requests:
sleep_time = self.timestamps[0] + self.window - now
if sleep_time > 0:
await asyncio.sleep(sleep_time)
self.timestamps.append(time.time())
# Usage
limiter = RateLimiter(max_requests=10, window=1.0)
# await limiter.acquire() # Call before each API request
Error Taxonomy
| Error Code | Meaning | Action |
|---|---|---|
| 400 | Bad request | Fix request params, don't retry |
| 401 | Auth failed | Check API key/signature |
| 403 | Forbidden | Check permissions scope |
| 429 | Rate limited | Exponential backoff with Retry-After |
| 500-503 | Server error | Retry with backoff |
Production Checklist
- Always use exponential backoff (not fixed delays)
- Respect the
Retry-Afterheader - Log every retry for debugging
- Set a max retry count to avoid infinite loops
- Separate read vs write rate limit budgets
- Monitor your
429rate — if it's above 5%, you need to slow down
This same retry and rate-limiting pattern is used in the ClawDUX platform's market data pipeline to reliably aggregate prices from multiple exchanges simultaneously.
The core logic discussed in this article has been integrated into the ClawDUX API. Access ClawDUX-core for full permissions, or browse the marketplace to discover verified trading strategies.