币安 API 报 -1021:快 1 秒或超出 recvWindow 的时间戳会被拒

-1021 是币安现货 API 的时间戳错误:请求带的 timestamp 要么比服务器时间快了 1 秒以上,要么旧到超出了 recvWindow,常见原因是本机时钟没对齐,或者脚本生成 timestamp 的位置不对。
「Timestamp for this request was 1000ms ahead of the server's time.」和「Timestamp for this request is outside of the recvWindow.」,这两条英文消息在币安官方文档里同属一个错误码:-1021 INVALID_TIMESTAMP。看到其中任何一条,说明服务器不接受这次请求带的时间戳,请求在转给撮合引擎之前就被拒了。
1. 两条消息指向两个相反的方向 #
币安官方现货 API 文档仓库 binance/binance-spot-api-docs 的 errors.md 里,-1021 下面列了两条消息,原文照录:
-1021 INVALID_TIMESTAMP
Timestamp for this request is outside of the recvWindow.
Timestamp for this request was 1000ms ahead of the server's time.
把消息字面和同一仓库 rest-api.md 里的服务器判断逻辑对照着读,两条各对应一个方向。这个对应关系是按字面推出来的,errors.md 本身没有逐条注明触发条件。
- 「1000ms ahead」:请求里的 timestamp 比服务器当前时间超前了 1 秒以上,相当于这个请求「来自未来」。最直接的原因是本机时钟比服务器快。
- 「outside of the recvWindow」:timestamp 太旧,服务器时间减去它已经超过了 recvWindow。本机时钟偏慢、网络单程耗时太长、timestamp 生成得太早,单独一项或几项叠加都会落到这条。
分清是哪一条,排查方向就定了一半:前一条要找谁把时间报快了,后一条要找时间差是在哪一段被拉长的。如果报的是 -1022 INVALID_SIGNATURE(原文「Signature for this request is not valid.」),那是签名校验没过,和时间戳是两个问题。
2. 服务器怎么判定一个时间戳能用 #
rest-api.md 的「Timing security」一节规定:SIGNED 请求除了签名,还必须带 timestamp 参数,值是当前时间戳,单位可以是毫秒或微秒。另有一个可选参数 recvWindow,指定请求在多长时间内有效,只接受毫秒,最多带三位小数来表示到微秒,文档举的例子是 6000.346。不传 recvWindow 时默认 5000 毫秒,最大 60000 毫秒。
图里那段伪代码,拆成中文是前后两道关:
- 收到请求时取一次服务器时间 serverTime。只有「timestamp 小于 serverTime 加 1000 毫秒」和「serverTime 减 timestamp 不超过 recvWindow」同时成立,服务器才开始处理。
- 开始处理之后再取一次 serverTime。这时 serverTime 减 timestamp 仍不超过 recvWindow,请求才转给撮合引擎;否则拒绝。
第一个条件管「太超前」,容差固定是 1 秒,和 recvWindow 设多少无关。第二个条件管「太旧」,上限就是 recvWindow。按公式,这个窗口从 timestamp 那一刻开始计时,而不是从请求到达服务器时开始,网络上耽搁的时间和服务器处理前后的时间都从同一个窗口里扣。
假设 recvWindow 用默认的 5000 毫秒,下面四种情形的数值是自拟的,单位都是毫秒。到达时的 serverTime 减 timestamp,等于单程网络耗时减去本机快出的量;本机慢的话,就是单程耗时加上慢的量。
| 情形 | 本机时钟 | 单程耗时 | 到达时 serverTime − timestamp | 结果 |
|---|---|---|---|---|
| A | 快 1,200 | 100 | 100 − 1,200 = −1,100 | 超前超过 1,000,报 ahead |
| B | 快 1,200 | 300 | 300 − 1,200 = −900 | 两个条件都满足,通过 |
| C | 慢 4,500 | 600 | 600 + 4,500 = 5,100 | 超过 5,000,报 outside recvWindow |
| D | 慢 4,500 | 300 | 300 + 4,500 = 4,800 | 第一道关通过,只剩 200 的余量 |
A 和 B 的时钟快得一样多,结果却相反,因为 B 的网络耗时抵掉了一部分超前量。照伪代码推下去,一台时钟快 1 秒出头的机器,会随网络快慢时好时坏地报 ahead,看上去像偶发故障。D 虽然过了第一道关,服务器开始处理后还要再取一次时间,余下的 200 毫秒一旦用完,请求照样在第二道关被拒。
3. 本机比服务器快还是慢?拿 /api/v3/time 量 #
rest-api.md 的「Check server time」对应 GET /api/v3/time:用来测试连通性,返回当前服务器时间,不带任何参数,权重为 1。
量法:发请求前记下本机时间 t0,收到响应后记下本机时间 t1,把 (t0 + t1) / 2 当作服务器生成 serverTime 那一刻的本机时间。偏差 = serverTime − (t0 + t1) / 2,往返时间 = t1 − t0。偏差为正,说明服务器比本机快,也就是本机慢;为负则本机快。取中点的前提是去程和回程耗时差不多,线路两头不对称时,量出来的偏差会带一点误差。
# 量本机与币安服务器的时钟偏差(示意)
import time
import requests
def local_ms():
return time.time() * 1000
for i in range(3):
t0 = local_ms()
server = requests.get("https://api.binance.com/api/v3/time").json()["serverTime"]
t1 = local_ms()
offset = server - (t0 + t1) / 2
print(f"偏差 {offset:+.0f} ms,往返 {t1 - t0:.0f} ms")
下面是 2026 年 9 月 23 日的一组演示数据:一台经代理联网的机器,连续 3 次请求币安公开行情镜像 data-api.binance.vision 上的 /api/v3/time,按上面的量法得出。
| 项目 | 三次结果(毫秒) | 三次平均 |
|---|---|---|
| 偏差 serverTime − (t0 + t1) / 2 | +792、+800、+812 | (792 + 800 + 812) / 3 ≈ +801 |
| 往返 t1 − t0 | 1,425、1,438、1,460 | (1,425 + 1,438 + 1,460) / 3 ≈ 1,441 |
偏差为正,这台机器的时钟比服务器慢约 0.8 秒。往返 1.4 秒偏大,是中间那层代理造成的。
用这组数估算它发签名请求时的情形:timestamp 比服务器时间早约 800 毫秒,单程耗时按往返的一半取约 720 毫秒(1,441 / 2),到达时 serverTime − timestamp 约为 800 + 720 = 1,520 毫秒,在默认的 5000 毫秒以内,不会触发 -1021。这台机器要报错,得是时钟慢的量加上单程耗时超过 recvWindow,报 outside recvWindow;或者反过来,时钟比服务器快 1 秒以上、网络耗时又没把这部分抵掉,报 ahead。你的机器请用上面的脚本另量。
4. 排查顺序:调大 recvWindow 排在最后 #
我们建议按这个顺序排查,前一步没排除,别急着跳到后一步。
- 认消息。报的是「1000ms ahead」,直接查本机时钟是不是偏快;这一条调 recvWindow 没用,那 1 秒的容差是固定的。报的是「outside of the recvWindow」,继续往下。
- 量偏差。用上一节的脚本连量几次。偏差接近 1 秒或更多,当成时钟问题处理;偏差很小、往返却很长,问题更可能在网络这一段。
- 打开操作系统的自动同步时间。同步完再量一次,确认偏差降下来了。脚本如果跑在云服务器或另一台电脑上,要量、要同步的是那台机器的时间。
- 查代码里 timestamp 在哪一步生成。每次签名前现取当前时间;重试时重新生成 timestamp 和签名,不复用第一次拼好的查询串。下一节有对照写法。
- 前四步都做了,网络确实慢,再把 recvWindow 适当调大。官方在 Timing security 一节加粗写着:「It is recommended to use a small recvWindow of 5000 or less! The max cannot go beyond 60,000!」
把调 recvWindow 放到最后,是因为这个窗口本身就是一道保护。同一节里有一句「Serious trading is about timing.」,后面解释:网络不稳定,请求到达服务器的时间有早有晚,recvWindow 用来规定请求必须在多少毫秒内被处理,超过就拒绝。窗口拉到 60000,意味着一个一分钟前签出的下单请求照样可以进撮合,那时的行情可能已经和你生成请求时看到的不一样了。
5. 让 AI 写签名代码时,盯住 timestamp 在哪一行生成 #
让 ChatGPT 或 Claude 写调用币安 API 的脚本时,timestamp 那一行很容易被一眼带过。下面两种是常见的写法错误示例,AI 生成的代码和手写代码里都可能出现。代码是伪代码,只演示 timestamp 在哪一步生成,不含真实 Key,也不是能直接下单的完整程序。
错误写法一,程序启动时生成一次 timestamp,之后每个请求都复用。假设本机时钟准、网络耗时忽略不计,启动 5 秒以后,serverTime − timestamp 就超过了默认的 5000;把 recvWindow 开到最大 60000,也只能撑到第 60 秒。脚本刚启动时那几个请求正常,跑一阵之后全部报 outside recvWindow,碰到这种现象,先查是不是这一种。
错误写法二,第一次请求拼好、签好之后,重试时把同一个查询串原样再发。第一次如果就是因为慢而报了 -1021,重发的 timestamp 只会更旧,重试几次结果都一样。只换 timestamp 不重算签名也不行,timestamp 是参与签名的参数之一,换了它,旧签名就和新参数对不上了。
# now_ms()、sign()、sign_all()、send()、is_error() 都是占位函数;Key 与 secret 从环境变量读取
# 错误写法一:启动时生成一次,之后一直复用
TS = now_ms()
def signed_request(path, params):
params["timestamp"] = TS # 永远是启动那一刻
params["signature"] = sign(params)
return send(path, params)
# 错误写法二:签好一次,重试时原样重发
query = sign_all({**params, "timestamp": now_ms()})
for attempt in range(3):
resp = send(path, query) # 每次都带着第一次的 timestamp
if not is_error(resp, -1021):
break
# 对照写法:每次发出前现取时间、现签名,重试从头走
def signed_request(path, params):
p = dict(params)
p["timestamp"] = now_ms() # 紧挨着签名生成
p["recvWindow"] = RECV_WINDOW # 配置项,默认 5000
p["signature"] = sign(p) # timestamp 参与签名,换了就重签
return send(path, p)
for attempt in range(3):
resp = signed_request(path, params)
if not is_error(resp, -1021):
break
对照写法里,重试调用的是整个 signed_request,每一轮都重新取时间、重新签名。时钟本身偏了的话,这样重试几次也都会失败,要回到上一节的第 2、3 步。
让 AI 写这部分代码时,可以把下面几条贴在需求前面:
写调用币安现货 API 的签名请求时,按下面三条来:
1. timestamp 在签名函数内部、紧挨着签名那一行生成,不要在程序启动时生成后复用。
2. 任何重试都重新生成 timestamp、重新签名,不要重发之前拼好的查询串。
3. recvWindow 写成配置项,默认 5000;遇到 -1021 时把完整报错消息打印出来,不要自动调大 recvWindow。
示例里的 API Key 和 secret 一律用占位符,从环境变量读取。
代码交回来后,在里面找到 timestamp 被赋值的那一行,看它是不是在签名函数里、会不会每次请求都执行。改好的脚本先放到现货测试网上跑,接法见AI 写的币安交易脚本先跑现货测试网;实盘用的 Key 权限照给 AI 开 Binance API:只开只读权限,提现权限绝不能开来开;哪些决定不该交给 AI 生成的循环去做,参考千万不要让 AI 做的 7 件事。
— PromptDeck, 2026-09-23