Key takeawaysGET /api/v3/time (weight 1, no key) returns serverTime in milliseconds — the clock every signed request is judged against.
GET /api/v3/time(weight 1, no key) returnsserverTimein milliseconds — the clock every signed request is judged against.- The acceptance rule is asymmetric: a fixed 1,000 ms tolerance for timestamps ahead of the exchange clock, and recvWindow for those behind it — default 5,000 ms, maximum 60,000 ms.
- Widening recvWindow never fixes the "1000ms ahead" flavor of
-1021; that one is a clock problem, and Binance recommends keeping the window at 5,000 ms or less because it doubles as a replay window. - Measure clock offset with round-trip compensation: the naive
serverTime - localTimeoverstated drift by ~134 ms from our German addresses and ~11 ms from Japanese ones — half the round trip, not a broken clock.
This summary was created with AI.
Reading the exchange clock
The server time endpoint is the simplest call on the API: GET /api/v3/time costs a weight of 1, needs no key, and returns a single field — serverTime in milliseconds. That number is the clock every signed request is judged against.
The mistake is in how people compare it to their own. The obvious approach subtracts local time from the response and calls the difference clock drift, but that quietly folds your network latency into the result: the reply you're holding was generated somewhere over the Pacific and spent half a round trip getting back to you. Measure it the way NTP does instead — bracket the call with local timestamps and compensate for the flight:
import time, requests
def clock_offset(base="https://api.binance.com", samples=10):
offsets = []
for _ in range(samples):
t0 = time.time() * 1000
server = requests.get(f"{base}/api/v3/time").json()["serverTime"]
t1 = time.time() * 1000
offsets.append(server - (t0 + t1) / 2) # midpoint cancels the round trip
naive = server - t1 # what the naive method would report
offsets.sort()
return offsets[len(offsets) // 2], naive # median offset, last naive reading
offset, naive = clock_offset()
print(f"true offset: {offset:+.0f} ms naive estimate: {naive:+.0f} ms")
What to look at: the compensated median is your actual clock difference, and it should sit within a few tens of milliseconds on any host running NTP. The gap between the two printed numbers is pure geography. We ran this from our own network in July 2026: from Japanese addresses the naive method overstated the offset by roughly 11 ms, from German ones by about 134 ms — in each case almost exactly half the round-trip time we measured for those regions. A European server "diagnosed" with the naive method looks like it has a broken clock when it has a perfectly good one.
Two practical rules follow. Keep the host's clock disciplined by NTP (chrony or systemd-timesyncd) rather than by patching timestamps in your code, and if you do apply an offset, measure it periodically in the background — not before every request, which spends weight and adds a round trip to your critical path for information that changes slowly.
How recvWindow decides whether a request is accepted
Every signed request carries timestamp, and may carry the recvWindow parameter, which tells the exchange how long that signature stays valid. The documented acceptance logic is worth reading literally, because it is not symmetric:
if (timestamp < (serverTime + 1000) && (serverTime - timestamp) <= recvWindow) {
// process request
} else {
// reject request
}
Read left to right. The first clause is the future tolerance, and it's a hard 1,000 ms that you cannot configure: a request whose timestamp sits more than a second ahead of the exchange clock is rejected no matter what. The second clause is the past tolerance, and that one is recvWindow — 5,000 ms by default, 60,000 ms maximum.
This asymmetry explains the single most common wasted afternoon in Binance integrations. Error -1021 arrives in two flavors: "Timestamp for this request was 1000ms ahead of the server's time" and "Timestamp for this request is outside of the recvWindow." They look like one problem and are not. Widening recvWindow does nothing for the first message — a fast clock is fixed by fixing the clock. It helps only with the second, where your request genuinely took too long between signing and arrival.
Which raises the question of how wide to set it. Binance's own guidance in the server time endpoint documentation is to keep recvWindow small — 5,000 ms or less — and the reason is security rather than performance: the window is how long a captured request stays replayable, so a 60-second window is a 60-second replay opportunity you granted yourself. Treat the maximum as an emergency measure for a genuinely bad link, not as a default. And note what the window is spent on: latency counts against it. A signed order from Europe burns roughly 135 ms of its budget just reaching Tokyo, before the engine does anything — comfortable inside 5,000 ms, but it means a link with occasional multi-second stalls will produce timestamp rejections that look random and aren't.
Testing connectivity before debugging the clock
Before assuming the clock, spend ten seconds proving the connection. The ping endpoint — GET /api/v3/ping, weight 1, empty JSON response — answers one question: can this machine reach this API at all? Run it, then the time call, then your signed request, and the first step that fails tells you which layer to debug:
curl -s -o /dev/null -w "%{http_code}\n" https://api.binance.com/api/v3/ping # reachable?
curl -s https://api.binance.com/api/v3/time # clock?
Ping fails or returns an unexpected status — the problem is the network, the host, or eligibility for your region, and nothing about timestamps will fix it. Ping succeeds but signed calls fail with -1021 — now the clock is a legitimate suspect, and the offset measurement above tells you whether it's really drift or just distance. Ping and time both succeed while signed calls fail with -1022 instead — that's the signature, not the clock: the parameter string you signed doesn't match the one you sent.
There's one more reason to keep this ladder in your startup routine rather than your memory. Timestamp failures cluster around events that have nothing to do with code — a VM resumed from suspend, a container host with a drifting clock, a redeployed worker in a new region. Logging the compensated offset and the round-trip time at startup, next to the egress IP, turns each of those into a one-line diagnosis instead of an afternoon.
Distance is the part of this you can actually design around. For an order path, every millisecond between signing and arrival is spent from both budgets — the recvWindow that decides whether the request is accepted, and the slippage that decides what it costs. That's why active traders put their egress close to the matching engine in Tokyo, and it's what our proxies for Binance are used for: dedicated IPv4 addresses with geo-targeting, static for the plan term, so the address in your IP whitelisting rules stays the address you connect from.