凌晨 3 点,你被一条告警叫醒。

告警信息只有一行:HTTP 429 Too Many Requests。你盯着屏幕,快速翻了翻项目文档——没有。你又搜了一圈 API 文档——没有。你叹了口气,试探性地加了个 sleep(60),跑了一下,过去了。三个月后,你忘了这件事,直到另一个类似的告警出现。

这不是某个程序员的黑历史。这是 API 错误处理领域的集体困境:HTTP 状态码能告诉你的,永远不够用。


当 HTTP 状态码不够用时

429 Too Many Requests 是一个有效的 HTTP 状态码,RFC 6585 在 2012 年定义了它。从语义上讲,它已经说清了问题:你请求太快了。但为什么在实际开发中,它常常让人抓狂?

第一层问题:语义模糊。

429 告诉你"太快了",但不告诉你"快了多少",也不告诉你"要等多长时间"。你被迫去读 Retry-After 响应头——如果服务器给了的话。不同的服务有不同的约定:有的用秒数,有的用时间戳,有的根本不给。这导致客户端代码里充斥着碎片化的判断逻辑。

第二层问题:无法区分错误类型。

一个完善的 API 系统,限频只是众多错误场景之一。你可能遇到:鉴权失败、数据不存在、参数校验错误、服务端内部错误、负载过载……它们都可能返回 4xx。如果所有限速都返回 429,所有鉴权失败也返回 401,那当你遇到一个 401 Unauthorized 时,你怎么知道是 API Key 填错了,还是 Key 被吊销了,还是 Key 压根没传?

在大型量化系统中,这种模糊性是灾难性的。你的风控模块需要知道"是超时了还是 Key 无效",你的重试逻辑需要知道"要不要等待",你的告警系统需要知道"这是客户端问题还是服务端问题"。HTTP 状态码做不到这种区分度。


TickDB 的错误码体系:3001 意味着什么

TickDB 定义了一套自己的错误码体系,运行在 HTTP 状态码之上。当你的请求遇到限频,HTTP 层面会收到 200 OK(技术上是 429 Too Many Requests,但 SDK 做了转换以保证兼容性),而响应体中会包含:

{
  "code": 3001,
  "message": "Request rate limit exceeded",
  "retry_after": 5
}

code: 3001 是一个明确的业务层错误码。它的含义是:请求频率超过限制,请在指定的 retry_after 秒之后重试。

这不是随意选的一个数字。这是一套体系。

错误码分段设计

TickDB 的错误码采用分段结构,每个区段服务不同的错误类型:

错误码范围 区段 含义
1001-1999 认证区段 API Key 相关错误
2001-2999 业务逻辑区段 数据不存在、参数错误
3001-3999 限频区段 请求频率控制
5001-5999 服务端区段 服务器内部错误

这种设计让你的代码可以这样写:

def handle_error(response, symbol=None):
    """TickDB 标准错误处理——分段识别,精准响应"""
    code = response.get("code", 0)
    retry_after = response.headers.get("Retry-After", 5)
    
    if code == 0:
        return response.get("data")
    
    if 1001 <= code <= 1999:
        # 认证区段:Key 无效、缺失、权限不足
        raise AuthError(f"API Key 错误 (code {code}): {response.get('message')}")
    
    if 2001 <= code <= 2999:
        # 业务区段:品种不存在、参数错误
        if code == 2002:
            raise KeyError(f"交易品种 {symbol} 不存在,请检查代码")
        raise ValueError(f"请求参数错误 (code {code}): {response.get('message')}")
    
    if code == 3001:
        # 限频区段:等待后重试
        actual_wait = max(int(retry_after), 1)
        print(f"触发限频,等待 {actual_wait} 秒后重试...")
        time.sleep(actual_wait)
        return None
    
    if 5001 <= code <= 5999:
        # 服务端区段:可能临时故障,不建议立即重试
        raise RuntimeError(f"服务端错误 (code {code}),请联系 support")
    
    raise RuntimeError(f"未知错误 {code}: {response.get('message')}")

注意看这段代码:你不需要猜测错误类型,不需要解析 message 文本,只需要判断 code 的区间。 这是统一错误码体系带来的第一个工程价值:可编程的错误分类


为什么是 3001 而不是 429?

你可能会问:既然 HTTP 已经定义了 429,为什么不直接用 429,而是要在响应体里塞一个 code: 3001

答案在于信息密度编程体验

429 的局限性

当你收到一个 429 时,你需要做以下事情才能正确处理它:

  1. 确认这是限频而不是其他 4xx(因为 429 本质上还是 4xx)
  2. 解析 Retry-After 头——它可能是整数秒数,也可能是 HTTP 日期
  3. 根据返回值决定等待策略
  4. 判断是客户端限频还是服务端整体限速(影响重试窗口)

而当你收到 code: 3001 时:

if code == 3001:
    time.sleep(retry_after)

一行代码,决策完成。

更深层的区别:层级分离

HTTP 状态码是传输层的信号,它解决的问题是:"这个 HTTP 响应成功了吗?"

业务错误码是应用层的信号,它解决的问题是:"这个业务请求发生了什么?"

限频是一个业务层概念,不是传输层概念。当你的请求被限频,HTTP 协议层面可能是成功的(连接建立、数据传输正常),但在业务层面,你的请求频率触发了限制。这种情况下,用 429 是一种勉强的映射,它把"业务问题"强行塞进了"传输层信号"。

TickDB 的做法是:让 HTTP 状态码回归传输语义(200 表示请求到达了服务器),让业务错误码承担业务语义。 这样你可以在同一个响应中同时获得传输状态和业务状态,信息不丢失,逻辑不耦合。


生产级代码:如何正确处理 3001

回到实战。以下是一个生产级的 TickDB WebSocket 连接代码,展示了如何正确处理限频错误:

import os
import time
import json
import random
import socket
import websocket

class TickDBClient:
    """TickDB WebSocket 客户端——生产级限频处理"""
    
    def __init__(self, api_key=None):
        self.api_key = api_key or os.environ.get("TICKDB_API_KEY")
        if not self.api_key:
            raise ValueError("请设置环境变量 TICKDB_API_KEY")
        self.ws = None
        self.retry_count = 0
        self.base_delay = 1
        self.max_delay = 60
        self.max_retries = 5
    
    def connect(self, endpoint="wss://api.tickdb.ai/ws"):
        """建立 WebSocket 连接,带重连和限频处理"""
        url = f"{endpoint}?api_key={self.api_key}"
        
        try:
            self.ws = websocket.create_connection(
                url,
                timeout=10,
                ping_interval=30,  # ⚠️ 30秒心跳保活
                ping_timeout=10
            )
            print(f"已连接到 TickDB WebSocket")
            self.retry_count = 0
            return True
        
        except websocket.WebSocketBadStatusException as e:
            if e.status_code == 401:
                raise AuthError("API Key 无效,请检查环境变量 TICKDB_API_KEY")
            raise ConnectionError(f"WebSocket 连接失败 (HTTP {e.status_code})")
        
        except Exception as e:
            self._handle_connection_error(e)
            return False
    
    def send_command(self, cmd):
        """发送命令并处理响应,包含限频重试"""
        if not self.ws:
            raise ConnectionError("WebSocket 未连接")
        
        try:
            self.ws.send(json.dumps(cmd))
            response = self.ws.recv()
            data = json.loads(response)
            
            # ⚠️ 核心:识别 3001 限频错误
            if data.get("code") == 3001:
                retry_after = int(data.get("retry_after", 5))
                print(f"[限频] 触发 3001,等待 {retry_after} 秒...")
                time.sleep(retry_after)
                return self.send_command(cmd)  # 重试一次
            
            if data.get("code") != 0 and data.get("code") is not None:
                raise RuntimeError(f"请求失败 (code {data.get('code')}): {data.get('message')}")
            
            return data.get("data")
        
        except websocket.WebSocketTimeoutException:
            raise ConnectionError("WebSocket 响应超时")
    
    def _handle_connection_error(self, error):
        """指数退避重连 + 抖动"""
        self.retry_count += 1
        
        if self.retry_count > self.max_retries:
            raise ConnectionError(f"重试次数超过上限 ({self.max_retries}),请检查网络或 API Key")
        
        # 指数退避
        delay = min(self.base_delay * (2 ** self.retry_count), self.max_delay)
        # 抖动:避免惊群效应
        jitter = random.uniform(0, delay * 0.1)
        wait_time = delay + jitter
        
        print(f"[重连] 第 {self.retry_count} 次重试,等待 {wait_time:.2f} 秒...")
        time.sleep(wait_time)
        
        self.connect()
    
    def subscribe_depth(self, symbol):
        """订阅订单簿深度数据"""
        return self.send_command({
            "cmd": "subscribe",
            "channel": "depth",
            "symbol": symbol,
            "depth": 10
        })
    
    def close(self):
        if self.ws:
            self.ws.close()
            print("WebSocket 连接已关闭")

这段代码的关键在于:

  1. 识别 3001if data.get("code") == 3001 精准定位限频场景
  2. 尊重 retry_aftertime.sleep(data.get("retry_after", 5)) 不猜测、不武断
  3. 指数退避 + 抖动:避免在限频恢复的瞬间所有客户端同时发起请求
  4. 心跳保活ping_interval=30 确保连接不被中间件丢弃

统一错误码体系的工程价值

回到最初的问题:3001 比 429 好在哪里?答案不只是"语义更清晰",而是整个错误处理范式的升级

价值一:可预测的代码分支

当你知道错误码的分段规则后,你可以写出这样的通用错误处理器:

# 不需要知道具体是哪个错误,只需要知道它的类别
def classify_error(code):
    if 1000 <= code < 2000:
        return "认证问题"
    if 2000 <= code < 3000:
        return "业务问题"
    if 3000 <= code < 4000:
        return "限频问题"
    if code >= 5000:
        return "服务端问题"

这种代码在任何 API 响应处理逻辑中都可以复用,不需要为每个错误码写单独的判断。

价值二:统一的日志格式

当所有错误都有标准化的 code 字段时,你的日志系统可以这样设计:

[2026-04-20 03:12:45] ERROR | code=3001 | msg="Rate limit exceeded" | retry_after=5 | symbol="NVDA.US"

这种结构化日志让告警规则、监控面板、事后复盘都变得简单。你可以用一条正则表达式匹配所有限频错误,但你很难用一条规则同时匹配 429、401 和 403。

价值三:错误处理的职责分离

在大型量化系统中,错误处理往往分散在多个模块:重试逻辑在连接层、业务错误在数据层、告警在监控层。统一错误码让各层可以独立运作:

  • 连接层:遇到 3001,触发等待重试
  • 数据层:遇到 2002,跳过该品种,继续处理其他标的
  • 监控层:任何 5001+,发送告警给 on-call

如果用 HTTP 状态码,你会发现"429 可能是限频,也可能是负载过高","401 可能是 Key 错误,也可能是权限不足",职责无法清晰分离。


如果你是习惯 AI 辅助开发的开发者

如果你习惯用 AI 工具加速开发,TickDB 也提供了标准化接入方式。在主流 AI 助手中搜索安装 tickdb-market-data SKILL,可以让你用自然语言查询 API 错误码的含义:

用户:我在用 TickDB 做港股订单流分析,请求的时候报错了,code 是 3001
AI:code 3001 表示请求频率超限。TickDB 的限频规则是 ...
     你需要在代码中读取 retry_after 字段,等待对应秒数后再重试。

这种体验背后依赖的,正是这套统一的错误码体系。没有标准化,AI 就无法可靠地帮你排查问题。


结语:你的代码不应该靠"猜"

API 错误处理是一个看似简单、实则复杂的问题。429 是一个好的开始,但它不够细;3001 是一个具体的答案,但你需要知道它背后的设计逻辑。

下次当你遇到 code: 3001 时,希望你不再需要翻文档、不再需要试错、不再需要凌晨 3 点被叫醒。你只需要知道:这是 TickDB 在告诉你,你的请求频率触及了边界,但系统已经为你准备好了回来的路——只需要等 5 秒。

这种确定性,才是好的开发者体验。


下一步行动

如果你需要在自己的项目中处理限频错误,访问 TickDB API 文档,查看完整的错误码参考表和代码示例。

如果你正在设计自己的 API 错误处理系统,建议参考 TickDB 的分段设计思路:从 1001 开始,按功能区段划分错误码,每个区段内部连续。这比混用 HTTP 状态码更容易维护。

如果你习惯用 AI 辅助开发,到你的 AI 助手中搜索安装 tickdb-market-data SKILL,用自然语言查询任何错误码的含义。


本文不构成任何投资建议。市场有风险,投资需谨慎。