Rate Limiting
Ticksupply uses a token bucket algorithm to enforce fair usage limits. This guide explains the limits, how to monitor your usage, and best practices for staying within bounds.
Rate limit overview
Window Limit Scope Per minute 200 requests Per account Per hour 10,000 requests Per account
Rate limits are shared across all API keys on the same account. If you have multiple applications using different keys, they share the same limits.
Every API response includes headers showing your current rate limit status:
Header Description X-RateLimit-Limit-MinuteMaximum requests allowed per minute X-RateLimit-Remaining-MinuteRequests remaining this minute X-RateLimit-Reset-MinuteUnix timestamp (seconds) when the minute window next has capacity. Included only when the minute bucket is exhausted. X-RateLimit-Limit-HourMaximum requests allowed per hour X-RateLimit-Remaining-HourRequests remaining this hour X-RateLimit-Reset-HourUnix timestamp (seconds) when the hour window next has capacity. Included only when the hour bucket is exhausted.
Example response headers on a normal call:
X-RateLimit-Limit-Minute: 200
X-RateLimit-Remaining-Minute: 195
X-RateLimit-Limit-Hour: 10000
X-RateLimit-Remaining-Hour: 9850
When a window is exhausted, the matching X-RateLimit-Reset-* header is added alongside the rest. Its value is an absolute Unix timestamp in seconds — not a delay:
X-RateLimit-Remaining-Minute: 0
X-RateLimit-Reset-Minute: 1776426360
Compute the wait time as reset - now (seconds), not by treating the value as a duration.
When rate limits are exceeded
When you exceed rate limits, the API returns a 429 Too Many Requests response:
{
"error" : {
"code" : "rate_limited" ,
"message" : "Rate limit exceeded. Retry after 30 seconds."
}
}
The response includes a Retry-After header indicating how many seconds to wait:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
Handling rate limits
Basic retry with exponential backoff
import time
import requests
def make_request_with_retry ( url , headers , max_retries = 5 ):
for attempt in range (max_retries):
response = requests.get(url, headers = headers)
if response.status_code == 429 :
retry_after = int (response.headers.get( "Retry-After" , 30 ))
print ( f "Rate limited. Waiting { retry_after } seconds..." )
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
raise Exception ( "Max retries exceeded" )
# Usage
data = make_request_with_retry(
"https://api.ticksupply.com/v1/exchanges" ,
{ "X-Api-Key" : API_KEY }
)
Proactive rate limiting
Monitor the rate limit headers to avoid hitting limits:
import time
class RateLimitedClient :
def __init__ ( self , api_key ):
self .api_key = api_key
self .remaining_minute = 200
self .reset_minute = 0
def request ( self , method , url , ** kwargs ):
# Check if we should wait
if self .remaining_minute <= 5 :
wait_time = max ( 0 , self .reset_minute - time.time()) + 1
if wait_time > 0 :
print ( f "Approaching limit, waiting { wait_time :.1f} s" )
time.sleep(wait_time)
headers = kwargs.pop( "headers" , {})
headers[ "X-Api-Key" ] = self .api_key
response = requests.request(method, url, headers = headers, ** kwargs)
# Update rate limit tracking
self .remaining_minute = int (
response.headers.get( "X-RateLimit-Remaining-Minute" , 200 )
)
# Reset-Minute is an absolute Unix timestamp (seconds) and is
# only present when the minute bucket is empty. When absent,
# fall back to "roughly one minute from now".
reset_header = response.headers.get( "X-RateLimit-Reset-Minute" )
self .reset_minute = (
int (reset_header) if reset_header else time.time() + 60
)
return response
Best practices
Batch requests when possible
Instead of making many individual requests, use endpoints that return multiple items:
# ❌ Avoid: Many individual requests
for subscription_id in subscription_ids:
response = requests.get(
f "https://api.ticksupply.com/v1/subscriptions/ { subscription_id } " ,
headers = headers
)
# ✅ Better: Single paginated request
response = requests.get(
"https://api.ticksupply.com/v1/subscriptions" ,
headers = headers,
params = { "limit" : 100 }
)
Use idempotency keys for retries
When retrying failed requests, use idempotency keys to prevent duplicate operations:
curl -X POST -H "X-Api-Key: YOUR_API_KEY" \
-H "Idempotency-Key: unique-request-id-123" \
-H "Content-Type: application/json" \
-d '{"datastream_id": 123}' \
https://api.ticksupply.com/v1/subscriptions
Implement request queuing
For high-volume applications, queue requests and process them at a sustainable rate:
import queue
import threading
import time
class RequestQueue :
def __init__ ( self , api_key , requests_per_second = 3 ):
self .api_key = api_key
self .delay = 1.0 / requests_per_second
self .queue = queue.Queue()
self .running = True
self .worker = threading.Thread( target = self ._process_queue)
self .worker.start()
def _process_queue ( self ):
while self .running:
try :
request_info, result_queue = self .queue.get( timeout = 1 )
response = requests.request( ** request_info)
result_queue.put(response)
time.sleep( self .delay)
except queue.Empty:
continue
def submit ( self , method , url , ** kwargs ):
kwargs[ "headers" ] = kwargs.get( "headers" , {})
kwargs[ "headers" ][ "X-Api-Key" ] = self .api_key
result_queue = queue.Queue()
self .queue.put(({ "method" : method, "url" : url, ** kwargs}, result_queue))
return result_queue.get()
Cache responses
Cache catalog data that doesn’t change frequently:
from functools import lru_cache
import time
@lru_cache ( maxsize = 1 )
def get_exchanges ():
response = requests.get(
"https://api.ticksupply.com/v1/exchanges" ,
headers = { "X-Api-Key" : API_KEY }
)
return response.json()
# Cache is reused for subsequent calls
exchanges = get_exchanges()
Exchange and stream type data changes infrequently. Cache these responses for at least 1 hour to reduce API calls.
Other limits
Rate limits control request throughput, but two separate kinds of limits can deny specific calls without consuming rate-limit budget:
Billing and plan limits — creating a new subscription, resuming a paused subscription, or starting an export beyond your plan returns 402 payment_required. See the Error Handling guide for the full list of causes.
Trial caps — on the trial plan, the number of active subscriptions and the cumulative successful-export size are both capped. Further POST /v1/subscriptions or POST /v1/exports calls return 402 payment_required once a cap is reached.
Neither is affected by Retry-After — retrying identical calls will keep failing until the underlying billing state changes. Upgrade your plan, update your payment method, or wait for your trial to convert in billing settings .
To check programmatically where you stand — current access_status, plan code, period end, and usage totals — call GET /v1/billing/summary .
Rate limit increases
If you need higher rate limits for your use case, contact our support team:
Rate limit increases are evaluated on a case-by-case basis and may require an upgraded plan.
Next steps
Error Handling Handle API errors gracefully in your application
Idempotency Safely retry requests using idempotency keys
Pagination Navigate large result sets efficiently
API Reference Explore the complete API documentation