Timestamps & clocks
Every row carries two timestamps: the venue's and ours. They answer different questions, and most timing bugs in crypto research come from using one where the other belongs.
Two clocks
ts_exchange | ts_capture | |
|---|---|---|
| Set by | The venue | Our capture host |
| Meaning | When the venue says the event happened (match time, book update time or block time, by venue) | When the message arrived at our network interface |
| Unit | int64 ns since epoch, UTC | int64 ns since epoch, UTC |
| Real precision | Venue-dependent: ms, µs or ns (table below) | ns resolution, accuracy under 50 µs |
| Clock quality | Maintained by the venue. We do not observe it directly, so we never adjust it. | PTP-disciplined, monitored every 16 seconds, out-of-budget intervals published |
| Nullable | Yes, for message types without a venue time | Only on venue_archive rows, which we did not capture |
| Comparable across venues | No | Yes, within one region |
Both are stored as integers. We never convert to floating-point seconds, because a float64 cannot hold a nanosecond epoch timestamp exactly (it keeps about 0.24 µs of resolution at today's dates).
How ts_capture is taken
ts_capture is the kernel receive timestamp of the network read that delivered the message, recorded with SO_TIMESTAMPING and carried through TLS decryption and parsing. Decompression, parsing and normalization all happen after the timestamp, so our own processing time never leaks into it. When one read delivers several venue messages, they share a ts_capture and keep their order through seq.
Clock synchronization
Capture hosts run on AWS Nitro instances with the Amazon Time Sync Service exposed as a PTP hardware clock on the network adapter. chrony disciplines the system clock against that hardware clock, and a separate monitor reads the clock error bound every 16 seconds. If the bound on any host exceeds 50 µs, the affected interval is listed with that day's quality metrics on Status, with start and end times on the same clock. The rows are kept; you decide whether to drop them.
Error budget
Worst-case contributions to the difference between ts_capture and true UTC on a Tokyo capture host, at the 99th percentile:
| Component | Budget | Notes |
|---|---|---|
| Reference to UTC | ≤ 25 µs | Time Sync Service bound for the hardware clock in the region. |
| Hardware clock to system clock | ≤ 5 µs | chrony offset, p99 over a trailing hour. |
| Timestamp point | ≤ 15 µs | Interrupt and softirq delay between packet arrival and the kernel stamp. Interrupt coalescing is disabled on capture interfaces. |
| Total, Tokyo (ap-northeast-1) | < 50 µs | Sum of the bounds above with headroom. |
| Total, Ireland (eu-west-1) | < 50 µs | Same configuration. |
| Total, N. Virginia (us-east-1) | < 50 µs | From Phase 2. |
A 50 µs bound is three orders of magnitude below the latency between a venue and our hosts, so it is fine for ordering events across venues and for measuring feed latency. It is not fine for comparing two events that our hosts received 10 µs apart; for those, treat the order as unknown unless they share a seq stream.
Venue timestamp precision
ts_exchange is always scaled to nanoseconds, but it is only as precise as the venue publishes. A millisecond venue's timestamps always end in six zeros. The field we read and its precision, by venue:
| Venue | Precision | Source field | Notes |
|---|---|---|---|
| Hyperliquid | ms | time, block time | L3 and L4 events carry the block timestamp; every event in a block shares it. |
| Binance | ms | T (trade, transaction), else E (event) | Futures depth updates carry both; we use transaction time T. |
| Bybit | ms | T, cts | cts is the matching engine time for book updates. |
| OKX | ms | ts | |
| Coinbase | µs | time (RFC 3339) | Six fractional digits. |
| Deribit | ms | timestamp | |
| Kraken | µs | timestamp (RFC 3339) | Spot. Kraken Futures publishes ms. |
| Bitget | ms | ts | |
| KuCoin | ns | time | Spot trades in ns; book updates and futures in ms. |
| Bitfinex | ms | MTS | |
| Bitstamp | µs | microtimestamp | |
| dYdX | ms | createdAt, block height | Block height is kept in seq. |
Every venue other than Hyperliquid is planned, subject to a data agreement with the exchange. Their rows follow each venue's public API documentation and will be confirmed with the venue before capture starts; any change is recorded in the changelog. Per-venue details are on Venues.
Which clock to use
| Task | Order or join on | Why |
|---|---|---|
| Replay one venue's book or tape | seq | The venue's own ordering. Millisecond timestamps tie constantly; seq never does. |
| Align several venues or datasets | ts_capture | One clock, one location. Venue clocks are not synchronized with each other. |
| Backtest what a strategy could have known | ts_capture | A message can only drive a decision after it arrives. Using ts_exchange here leaks future information equal to the feed latency. |
| Match venue-defined events (funding settlement, candle buckets, venue reports) | ts_exchange | These are defined on the venue's clock. |
| Measure feed latency | ts_capture − ts_exchange | Includes venue clock error; see below. |
Extended fields are computed on ts_capture for the same reason, and published on a 100 ms grid of that clock. See Extended fields.
As-of join across venues
import polars as pl
from fdc import Client
fdc = Client()
window = dict(symbol="BTC-PERP", start="2027-09-01T00:00:00Z", end="2027-09-01T01:00:00Z")
hl = fdc.trades(venue="hyperliquid", **window).to_polars()
bn = (fdc.trades(venue="binance", **window).to_polars()
.select("ts_capture", pl.col("price").alias("binance_price")))
# for each Hyperliquid trade, the last Binance trade we had received by then
joined = hl.sort("ts_capture").join_asof(
bn.sort("ts_capture"),
on="ts_capture", strategy="backward",
)Both venues in this example are captured in Tokyo, so ts_capture on both sides comes from the same region. Joining on ts_exchange instead would mix two venue clocks with different precision and unknown offsets.
Multi-region capture
Each venue is captured independently from two regions: Tokyo (ap-northeast-1, primary) and Ireland (eu-west-1, secondary), with N. Virginia (us-east-1) added in Phase 2. The streams are merged by venue sequence number. In the merged stream, ts_capture is the earliest receive time across the regions, and the region column names the region that received the message first.
- Earliest receive wins. When both regions have a message, the row keeps the earlier of the two receive times. For a Tokyo-hosted venue that is Tokyo for all but a few messages in ten thousand, usually during a brief stall on the Tokyo side.
- Secondary fills. When only the secondary region has a message, that row is used and marked
region = euw1. Itsts_captureis honest Ireland time, which for a Tokyo-hosted venue is roughly 105 ms later than Tokyo would have recorded. We do not adjust it. - Consequence for latency work. Filter to
region = apne1before computing latency statistics, or the secondary fills will fatten the tail. - Consequence for ordering. A secondary fill can carry a
ts_capturelater than the next primary row. Replays that need venue order should sort byseq.
The merge procedure and how often secondary fills occur are on Data quality.
Latency distribution
The difference ts_capture − ts_exchange combines the venue's internal delay, network transit and the venue clock's error. The example below shows the shape for Hyperliquid BTC-PERP trades captured in Tokyo.
| Capture region | p50 | p90 | p99 | p99.9 |
|---|---|---|---|---|
| Tokyo (apne1) | 18.4 ms | 40.7 ms | 77.8 ms | 125.0 ms |
| Ireland (euw1) | 124.1 ms | 147.9 ms | 189.3 ms | 262.4 ms |
SYNTHETIC: illustrative figures that show the shape of the distribution.
Three things to expect when you compute this yourself:
- Venue time is often the block or batch time. On Hyperliquid every event in a block shares a timestamp, so latency within a block varies with the event's position in it.
- Millisecond truncation adds up to 1 ms. A venue that truncates to milliseconds reports the event up to 999 µs early, which inflates measured latency by that amount.
- Negative values can happen. If a venue clock and ours differ by more than the feed latency, some rows show
ts_capturebeforets_exchange. We publish them as recorded and never clamp or shift venue timestamps.
For a longer treatment of measured latency and clock offsets, see Capture versus exchange timestamps.
Live stream timing
On the WebSocket stream, rows carry the same ts_capture as in the historical archive, so live and historical data line up. The rare exception is a message that reached the other region earlier but arrived at the live merger later; the historical merge then records the earlier time. Our target for added delivery latency is under 5 ms at the median, measured from ts_capture to the moment the message is written to your connection in the same region. Each delivered message also carries ts_send, our send time on the same clock, so you can measure that leg yourself; see WebSocket.