Every developer has encountered a 429 error at some point. The server says "too many requests," you wait a few seconds, and you retry. It works. But when you encounter a 3001 error code, the first instinct is often confusion — is this a rate limit? A server error? Did I format something wrong?
That confusion is precisely the problem TickDB set out to solve with its unified error code system. In this article, we dissect why TickDB uses error codes like 3001 instead of the familiar HTTP 429, how the system works end-to-end, and what it means for your integration's reliability.
The Problem with Mixing Protocol and Application Error Codes
HTTP status codes and application-level error codes serve fundamentally different purposes. HTTP status codes describe the result of an HTTP transaction — 200 means the server delivered something, 404 means the resource does not exist, 429 means you have exceeded the server's rate limit for that endpoint. Application error codes, on the other hand, describe the result of an operation within the API's business logic — invalid symbols, missing authentication scopes, data unavailability.
When you mix these two layers, you create ambiguity. Consider what happens when a rate limit is triggered on a WebSocket connection versus a REST endpoint:
Scenario A: HTTP 429 on REST endpoint
HTTP/1.1 429 Too Many Requests
Retry-After: 3
Scenario B: HTTP 429 on WebSocket upgrade
The connection fails silently, or you receive a 426 Upgrade Required
Neither scenario tells you why the rate limit was triggered or what to do about it. You receive a generic status code, but no structured information about which API endpoint was rate-limited, whether the limit is per-second or per-minute, or what the reset window looks like.
Generic HTTP error codes are optimized for browsers and proxies. APIs consumed by programmatic clients need something more precise.
TickDB's Error Code Architecture
TickDB separates error domains into three distinct tiers:
| Tier | Code range | Purpose | Example |
|---|---|---|---|
| Tier 1 | 1000–1999 | Authentication and authorization | 1001: Invalid API key |
| Tier 2 | 2000–2999 | Resource and data errors | 2002: Symbol not found |
| Tier 3 | 3000–3999 | Rate limits and quota management | 3001: Rate limit exceeded |
This three-tier structure is intentional. When an error arrives, the code alone tells you the domain of the problem:
- Code 1000s: Something is wrong with who you are.
- Code 2000s: Something is wrong with what you asked for.
- Code 3000s: Something is wrong with how often you asked.
A developer debugging an integration can triage the issue immediately by reading the first digit.
Why 3001 and Not 429?
The HTTP 429 status code exists in the HTTP specification precisely for rate limiting. So why did TickDB choose 3001 instead?
The answer lies in what the HTTP status code communicates to the client versus what the application needs to communicate internally.
HTTP 429 is a transport-layer signal. It tells the HTTP client to back off. It does not tell your application logic what the rate limit policy is, which endpoint triggered it, or how to handle the next retry intelligently.
TickDB 3001 is an application-layer signal. It tells your application that the rate limit has been exceeded, provides structured metadata about the limit, and allows your client library to implement intelligent retry logic.
Consider the actual response structure when a 3001 error occurs:
{
"code": 3001,
"message": "Rate limit exceeded for endpoint /v1/market/kline",
"data": null,
"request_id": "req_7f3a9c2d"
}
Now compare this to a raw HTTP 429 response:
HTTP/1.1 429 Too Many Requests
Retry-After: 5
The HTTP 429 response tells you to wait. The TickDB 3001 response tells you which endpoint hit its limit, allows you to correlate the error with a specific request ID, and integrates into a broader error-handling system that also covers authentication and resource errors.
When you are building a trading system that calls 10 different endpoints across REST and WebSocket connections, you need a unified error handling layer that treats all failure modes consistently. Mixing HTTP status codes with application codes would require your error handler to implement two different code paths — one for REST responses and one for WebSocket messages. TickDB's 3001 eliminates that split.
The Retry-After Standard in Practice
The Retry-After header is the standard HTTP mechanism for communicating backoff windows. TickDB implements this header for rate limit errors, but with a critical difference: the value is always expressed in seconds and is always included in the response headers, regardless of whether you are using REST or WebSocket.
import os
import time
import requests
API_KEY = os.environ.get("TICKDB_API_KEY")
BASE_URL = "https://api.tickdb.ai/v1"
def fetch_kline_with_retry(symbol, interval="1m", limit=100, max_retries=3):
"""
Fetch OHLCV data with retry logic that respects Retry-After headers.
⚠️ This implementation uses the synchronous requests library.
For high-frequency trading systems, replace with aiohttp for
non-blocking I/O. The retry logic remains identical.
"""
headers = {"X-API-Key": API_KEY}
params = {"symbol": symbol, "interval": interval, "limit": limit}
for attempt in range(max_retries):
try:
response = requests.get(
f"{BASE_URL}/market/kline",
headers=headers,
params=params,
timeout=(3.05, 10) # (connect_timeout, read_timeout)
)
if response.status_code == 200:
return response.json()
# Parse TickDB application error code from response body
body = response.json()
error_code = body.get("code", 0)
if error_code == 3001:
# Rate limit — read Retry-After and back off
retry_after = int(response.headers.get("Retry-After", 5))
print(f"Rate limited. Waiting {retry_after} seconds before retry.")
time.sleep(retry_after)
continue
# Non-retryable error — raise immediately
handle_error(error_code, body.get("message"))
except requests.exceptions.Timeout:
print(f"Request timed out on attempt {attempt + 1}. Retrying...")
time.sleep(2 ** attempt) # Simple exponential backoff for timeouts
continue
raise RuntimeError(f"Failed after {max_retries} attempts")
def handle_error(code, message):
"""Route TickDB error codes to appropriate handling."""
if code in (1001, 1002):
raise ValueError(f"Authentication failed: {message}")
if code == 2002:
raise KeyError(f"Symbol not found: {message}")
if code == 3001:
raise RuntimeError(f"Rate limit policy issue: {message}")
raise RuntimeError(f"Unexpected error {code}: {message}")
This pattern — reading the error code from the JSON body and the backoff window from the headers — works identically for REST and WebSocket error messages. The unified error model means your error handler does not need to branch on protocol type.
A Comparative Look: TickDB vs. Other Market Data APIs
| Error handling dimension | Generic REST API | HTTP-only error API | TickDB |
|---|---|---|---|
| Rate limit error code | HTTP 429 | HTTP 429 | 3001 |
| Authentication error code | HTTP 401 | HTTP 401 | 1001 / 1002 |
| Symbol not found code | HTTP 404 | HTTP 404 | 2002 |
| Structured error body | Optional | Sometimes | Always |
| Consistent code across REST and WebSocket | No | No | Yes |
| Retry-After header on rate limit | Sometimes | Sometimes | Always |
The practical impact is significant. When you integrate a market data API into a production trading system, you need error handling that works reliably at 3 AM when something goes wrong. With a unified code system, your on-call engineer can read the first digit of the error code — 1, 2, or 3 — and immediately know the category of the problem. With a mixed HTTP-and-application system, they need to know whether the error came from the HTTP layer or the application layer first.
The Developer Experience Benefits
Faster debugging. Error codes that map to specific domains eliminate the need to parse error messages. When your monitoring system alerts you to a spike in errors, you can segment by code prefix without reading a single message.
Consistent retry logic. Because all rate limit errors share code 3001 regardless of which endpoint triggered them, your retry function can be written once and reused across the entire API surface.
Better observability. Structured error responses with request IDs and error codes integrate cleanly into log aggregation systems. You can build dashboards that show error rate by type — authentication failures, bad symbols, rate limits — without needing to parse free-text messages.
Cross-protocol consistency. If you use both REST endpoints and WebSocket subscriptions in your system, you encounter the same error codes in both contexts. The mental model is simpler.
Implementing a Production-Grade Error Handler
Below is a more complete error handler that demonstrates the full scope of the TickDB error code system:
import time
import random
import logging
from typing import Optional, Callable, Any
import requests
logger = logging.getLogger(__name__)
class TickDBError(Exception):
"""Base exception for all TickDB errors."""
def __init__(self, code: int, message: str, request_id: Optional[str] = None):
self.code = code
self.message = message
self.request_id = request_id
super().__init__(f"[{code}] {message} (request_id: {request_id})")
class AuthenticationError(TickDBError):
"""Raised for codes 1001 and 1002."""
pass
class ResourceError(TickDBError):
"""Raised for codes 2000–2999."""
pass
class RateLimitError(TickDBError):
"""Raised for code 3001."""
def __init__(self, code, message, request_id, retry_after: int):
self.retry_after = retry_after
super().__init__(code, message, request_id)
def make_request(
method: str,
url: str,
headers: dict,
params: Optional[dict] = None,
json: Optional[dict] = None,
max_retries: int = 3
) -> dict:
"""
Generic request handler with exponential backoff, jitter, and
TickDB error code routing.
⚠️ For high-frequency use cases, consider replacing requests with
httpx or aiohttp. The error handling logic remains identical.
"""
base_delay = 1.0
max_delay = 30.0
for attempt in range(max_retries):
try:
response = requests.request(
method=method,
url=url,
headers=headers,
params=params,
json=json,
timeout=(3.05, 10)
)
# Success — return parsed body
if response.status_code == 200:
return response.json()
# Parse application error code from body
body = response.json()
code = body.get("code", 0)
message = body.get("message", "Unknown error")
request_id = body.get("request_id")
# Route by error domain
if code in (1001, 1002):
raise AuthenticationError(code, message, request_id)
if 2000 <= code < 3000:
raise ResourceError(code, message, request_id)
if code == 3001:
retry_after = int(response.headers.get("Retry-After", 5))
raise RateLimitError(code, message, request_id, retry_after)
# Unknown error — do not retry
raise TickDBError(code, message, request_id)
except requests.exceptions.Timeout:
logger.warning(f"Request timed out on attempt {attempt + 1}")
if attempt == max_retries - 1:
raise
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
time.sleep(delay + jitter)
continue
except requests.exceptions.RequestException as e:
logger.error(f"Network error: {e}")
raise
raise RuntimeError("Request loop exited unexpectedly")
This handler demonstrates the core principle: error handling by code domain, not by endpoint. Whether you call /v1/market/kline, /v1/market/depth, or any other endpoint, the error codes follow the same hierarchy and the same handling logic.
When 3001 Becomes a Feature, Not a Bug
Once you internalize the TickDB error code system, 3001 stops feeling like an arbitrary number and starts functioning as a control signal. A trading system that monitors its own error rate by code type can automatically adjust its request cadence — backing off when 3001 errors spike, alerting when 1001 errors indicate a key rotation issue, and logging 2002 errors to catch stale symbol lists.
The unified error code system is not just about consistency. It is about building systems that can reason about their own failures.
Summary: The Design Philosophy
TickDB uses 3001 instead of HTTP 429 because HTTP status codes are designed for the HTTP transport layer, while 3001 belongs to the application layer where your integration code lives. By keeping these layers separate and mapping all error domains to a consistent three-tier code structure, TickDB delivers three concrete benefits:
- Faster triage. The first digit of the code immediately identifies the error domain.
- Consistent retry logic. One handler handles all rate limit errors across all endpoints and protocols.
- Better observability. Error code segmentation integrates cleanly into monitoring and alerting pipelines.
The Retry-After header remains the backoff specification, but it is always accompanied by the structured error body that tells you exactly which endpoint triggered the limit and which request ID to reference in logs.
Next Steps
If you are integrating TickDB into a trading system, the error handling patterns demonstrated in this article should be implemented from day one. Start with the three-tier code structure as your mental model — authenticate first, validate symbols second, handle rate limits third.
If you want to see this in action, sign up at tickdb.ai to receive a free API key. The error responses documented here apply to all endpoints — REST and WebSocket — and the code examples in this article are directly runnable against a live account.
If you are evaluating enterprise data plans, the rate limit policies and error code behaviors described here apply across all tiers. Reach out to enterprise@tickdb.ai for dedicated quota management and SLA-backed response guarantees.
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace to get pre-built error handling templates and retry logic for TickDB integrations.
This article does not constitute investment advice. Market data APIs and trading systems involve technical and financial risk; past performance of any strategy does not guarantee future results.