py_clob_client Exponential Backoff: Rate Limit Handling Guide
How to implement exponential backoff with py-clob-client for handling rate limits, network errors, and API failures. Polymarket API rate limits require proper retry logic to prevent your trading bots from being blocked. This comprehensive 2026 guide covers exponential backoff strategies, using the tenacity library, custom implementations, and best practices for production trading bots.
Exponential backoff is essential for production Polymarket bots. When the API returns 429 (Too Many Requests) or network errors occur, exponential backoff gradually increases wait times between retries (2s, 4s, 8s, 16s...), preventing API overload and reducing the chance of permanent rate limit blocks. High search volume: "py_clob_client exponential backoff", "polymarket rate limit" - developers frequently search for retry strategies. Professional developers use analytics platforms like PolyTrack Pro which includes built-in rate limit handling, allowing them to focus on trading logic rather than API retry mechanics.
🔑 Why Exponential Backoff is Critical
- • Rate Limit Protection: Polymarket API returns 429 when rate limits are exceeded
- • Network Resilience: Handles temporary network errors and timeouts gracefully
- • Prevents API Overload: Gradually increases delays instead of hammering the API
- • Reduces Permanent Blocks: Avoids aggressive retry patterns that can trigger bans
- • Production Requirement: Essential for any bot that runs 24/7
Understanding Polymarket Rate Limits
Polymarket API has rate limits that vary by endpoint. Common limits include:
- • Gamma API: ~100 requests per minute recommended
- • CLOB API: Varies by endpoint, typically higher for trading operations
- • 429 Status Code: Returned when rate limit exceeded
- • Retry-After Header: Sometimes included to indicate when to retry
⚠️ Rate Limit Consequences
Exceeding rate limits repeatedly can result in temporary or permanent API access restrictions. Always implement exponential backoff to avoid this.
Using tenacity Library (Recommended)
The tenacity library is the industry standard for Python retry logic. It provides decorators and utilities for implementing exponential backoff with minimal code.
Installation
pip install tenacity py-clob-clientBasic Exponential Backoff
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
before_sleep_log,
after_log
)
import logging
from py_clob_client.client import ClobClient
from py_clob_client.exceptions import RateLimitError, APIError
# Setup logging
logger = logging.getLogger(__name__)
# Configure ClobClient
client = ClobClient(
api_key=os.getenv("POLYMARKET_API_KEY"),
api_secret=os.getenv("POLYMARKET_API_SECRET"),
base_url="https://clob.polymarket.com"
)
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=2, min=2, max=60),
retry=retry_if_exception_type((RateLimitError, APIError)),
before_sleep=before_sleep_log(logger, logging.WARNING),
after=after_log(logger, logging.INFO)
)
def get_orderbook_with_backoff(token_id: str):
"""Get orderbook with automatic exponential backoff on rate limits."""
try:
return client.get_orderbook(token_id)
except (RateLimitError, APIError) as e:
logger.warning(f"API error: {e}, retrying...")
raise # Re-raise to trigger retry
# Usage
try:
orderbook = get_orderbook_with_backoff("0x123...")
print(f"Best bid: {orderbook['bids'][0]}")
except Exception as e:
logger.error(f"Failed after retries: {e}")Advanced Configuration with Jitter
Add jitter (random delay) to prevent thundering herd problems when multiple bots retry simultaneously:
from tenacity import wait_exponential_jitter
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(initial=2, max=60, jitter=1),
retry=retry_if_exception_type(RateLimitError)
)
def place_order_with_backoff(order):
"""Place order with exponential backoff and jitter."""
return client.create_order(order)
# wait_exponential_jitter parameters:
# - initial: Starting delay in seconds (2s)
# - max: Maximum delay in seconds (60s)
# - jitter: Random jitter range (+/- 1s)Conditional Retries
Only retry on specific error conditions:
from tenacity import retry_if_exception, retry_if_result
def is_rate_limit_error(exception):
"""Check if exception is a rate limit error."""
return isinstance(exception, RateLimitError) or (
hasattr(exception, 'status_code') and exception.status_code == 429
)
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=2, min=2, max=60),
retry=retry_if_exception(is_rate_limit_error),
reraise=True
)
def get_markets_with_conditional_retry():
"""Only retry on rate limit errors, not other exceptions."""
return client.get_markets()See What Whales Are Trading Right Now
Get instant alerts when top traders make moves. Track P&L, win rates, and copy winning strategies.

Free forever. No credit card required.
Custom Exponential Backoff Implementation
For more control or if you prefer not to use tenacity, implement custom exponential backoff:
Synchronous Implementation
import time
import random
import logging
from typing import Callable, Any
from py_clob_client.exceptions import RateLimitError
logger = logging.getLogger(__name__)
def exponential_backoff_with_jitter(attempt: int, base_delay: float = 2.0, max_delay: float = 60.0) -> float:
"""
Calculate exponential backoff delay with jitter.
Args:
attempt: Current retry attempt (0-indexed)
base_delay: Base delay in seconds
max_delay: Maximum delay cap in seconds
Returns:
Delay in seconds
"""
# Exponential: base_delay * (2^attempt)
delay = base_delay * (2 ** attempt)
# Cap at max_delay
delay = min(delay, max_delay)
# Add jitter: random value between 0 and delay * 0.1
jitter = random.uniform(0, delay * 0.1)
delay += jitter
return delay
def retry_with_exponential_backoff(
func: Callable[[], Any],
max_retries: int = 5,
base_delay: float = 2.0,
max_delay: float = 60.0,
retry_exceptions: tuple = (RateLimitError,)
) -> Any:
"""
Retry a function with exponential backoff.
Args:
func: Function to retry
max_retries: Maximum number of retry attempts
base_delay: Starting delay in seconds
max_delay: Maximum delay cap in seconds
retry_exceptions: Tuple of exceptions to retry on
Returns:
Function result
Raises:
Last exception if all retries fail
"""
last_exception = None
for attempt in range(max_retries):
try:
return func()
except retry_exceptions as e:
last_exception = e
if attempt < max_retries - 1:
delay = exponential_backoff_with_jitter(attempt, base_delay, max_delay)
logger.warning(
f"Attempt {attempt + 1}/{max_retries} failed: {e}. "
f"Retrying in {delay:.2f}s..."
)
time.sleep(delay)
else:
logger.error(f"All {max_retries} attempts failed")
raise
# Should never reach here, but just in case
if last_exception:
raise last_exception
# Usage
def get_orderbook():
return client.get_orderbook("0x123...")
try:
orderbook = retry_with_exponential_backoff(get_orderbook)
except RateLimitError as e:
print(f"Rate limit error: {e}")Async Implementation
import asyncio
import random
from typing import Callable, Coroutine, Any
async def async_exponential_backoff(
func: Callable[[], Coroutine[Any, Any, Any]],
max_retries: int = 5,
base_delay: float = 2.0,
max_delay: float = 60.0
) -> Any:
"""
Async retry with exponential backoff.
"""
for attempt in range(max_retries):
try:
return await func()
except RateLimitError as e:
if attempt < max_retries - 1:
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
await asyncio.sleep(delay + jitter)
else:
raise
# Usage
async def fetch_market():
# Assuming async client method
return await async_client.get_market("market-slug")
try:
market = await async_exponential_backoff(fetch_market)
except RateLimitError as e:
print(f"Failed after retries: {e}")Respecting Retry-After Headers
Some APIs include a Retry-After header indicating when to retry. Always respect this header:
import time
from datetime import datetime, timedelta
from py_clob_client.exceptions import RateLimitError
def get_retry_after_seconds(response) -> float:
"""Extract Retry-After header value in seconds."""
retry_after = response.headers.get('Retry-After')
if retry_after:
try:
# Can be seconds or HTTP date
return float(retry_after)
except ValueError:
# Parse HTTP date
retry_time = datetime.strptime(retry_after, '%a, %d %b %Y %H:%M:%S GMT')
return (retry_time - datetime.utcnow()).total_seconds()
return None
@retry(
stop=stop_after_attempt(5),
wait=lambda retry_state: wait_exponential(
multiplier=2,
min=2,
max=60
)(retry_state),
retry=retry_if_exception_type(RateLimitError)
)
def get_orderbook_with_retry_after(token_id: str):
"""Respect Retry-After header if present."""
try:
response = client._make_request('GET', f'/book?token_id={token_id}')
return response.json()
except RateLimitError as e:
# Check if response has Retry-After header
if hasattr(e, 'response') and e.response:
retry_after = get_retry_after_seconds(e.response)
if retry_after:
logger.info(f"Respecting Retry-After: {retry_after}s")
time.sleep(retry_after)
raiseComplete Production Example
from tenacity import (
retry,
stop_after_attempt,
wait_exponential_jitter,
retry_if_exception_type,
stop_after_delay,
before_sleep_log
)
import logging
from py_clob_client.client import ClobClient
from py_clob_client.exceptions import RateLimitError, APIError
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class ResilientClobClient:
"""ClobClient wrapper with built-in exponential backoff."""
def __init__(self, api_key: str, api_secret: str):
self.client = ClobClient(
api_key=api_key,
api_secret=api_secret,
base_url="https://clob.polymarket.com"
)
@retry(
stop=(stop_after_attempt(5) | stop_after_delay(30)),
wait=wait_exponential_jitter(initial=2, max=60, jitter=1),
retry=retry_if_exception_type((RateLimitError, APIError)),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True
)
def get_orderbook(self, token_id: str):
"""Get orderbook with automatic retry."""
return self.client.get_orderbook(token_id)
@retry(
stop=stop_after_attempt(3), # Fewer retries for trading operations
wait=wait_exponential(multiplier=2, min=1, max=10),
retry=retry_if_exception_type(RateLimitError),
reraise=True
)
def create_order(self, order):
"""Create order with retry (faster for time-sensitive operations)."""
return self.client.create_order(order)
@retry(
stop=stop_after_attempt(10), # More retries for data fetching
wait=wait_exponential_jitter(initial=2, max=60, jitter=1),
retry=retry_if_exception_type((RateLimitError, APIError)),
reraise=True
)
def get_markets(self, active: bool = True):
"""Get markets with generous retry policy."""
return self.client.get_markets(active=active)
# Usage
client = ResilientClobClient(
api_key=os.getenv("POLYMARKET_API_KEY"),
api_secret=os.getenv("POLYMARKET_API_SECRET")
)
try:
orderbook = client.get_orderbook("0x123...")
markets = client.get_markets(active=True)
except Exception as e:
logger.error(f"Operation failed after retries: {e}")Best Practices
- • Start with 2-second delay: Base delay of 2 seconds is reasonable for most APIs
- • Double each attempt: Standard exponential backoff (2s, 4s, 8s, 16s...)
- • Add jitter: Random delay prevents thundering herd when multiple bots retry
- • Cap maximum delay: Cap at 60 seconds to avoid excessively long waits
- • Respect Retry-After headers: Use server-suggested retry times when available
- • Log retry attempts: Essential for debugging and monitoring
- • Different strategies per operation: Trading operations need fewer retries (time-sensitive), data fetching can use more
- • Stop after max attempts or time: Prevent infinite retries with time limits
- • Only retry on retryable errors: Don't retry on 4xx client errors (except 429)
- • Use tenacity for production: Battle-tested library with excellent features
Monitoring and Metrics
Track retry metrics to monitor API health and identify rate limit issues:
from tenacity import RetryCallState
import time
retry_count = 0
total_retry_delay = 0.0
def log_retry(retry_state: RetryCallState):
"""Log retry attempts for monitoring."""
global retry_count, total_retry_delay
retry_count += 1
attempt_number = retry_state.attempt_number
outcome = retry_state.outcome
if outcome:
exception = outcome.exception()
logger.warning(
f"Retry attempt {attempt_number}: {type(exception).__name__}: {exception}"
)
# Track total delay for metrics
if hasattr(retry_state, 'next_action'):
delay = retry_state.next_action.sleep
total_retry_delay += delay
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=2, min=2, max=60),
retry=retry_if_exception_type(RateLimitError),
before_sleep=log_retry
)
def tracked_get_orderbook(token_id: str):
"""Get orderbook with retry tracking."""
return client.get_orderbook(token_id)
# Monitor metrics
print(f"Total retries: {retry_count}")
print(f"Total retry delay: {total_retry_delay:.2f}s")Conclusion
Exponential backoff is essential for production Polymarket bots. Use the tenacity library for simple, reliable retry logic, or implement custom backoff for fine-grained control. Always add jitter, respect Retry-After headers, log retry attempts, and use different strategies for different operation types (trading vs data fetching).
For production applications, consider using platforms like PolyTrack Pro which includes built-in rate limit handling and retry logic, allowing you to focus on trading strategies rather than API mechanics.
Related Resources
Frequently Asked Questions
Exponential backoff gradually increases wait times between retries (2s, 4s, 8s, 16s...) when API rate limits are hit. It's essential for production bots to prevent 429 errors, handle network failures gracefully, and avoid permanent API blocks. Without it, aggressive retries can trigger rate limit bans.
Related Articles
Stop Guessing. Start Following Smart Money.
Get instant alerts when whales make $10K+ trades. Track P&L, win rates, and copy winning strategies.