API & Python

Coinbase API Rate Limit and Error Handling: The Elegant Solution

ClawDUX TeamMarch 22, 20267 min read0 views

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

python
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:

python
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

  1. Always use exponential backoff (not fixed delays)
  2. Respect the Retry-After header
  3. Log every retry for debugging
  4. Set a max retry count to avoid infinite loops
  5. Separate read vs write rate limit budgets
  6. Monitor your 429 rate — 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.

#coinbase#api#rate-limiting#error-handling#python

Related Articles