Every v2 event includes two Unix epoch timestamps in microseconds:
detected_time_us: when NLF detected the source eventsent_time_us: the endpoint's publication timestamp
The endpoints record publication at different points. Fast sets sent_time_us before any configured plan delay. Full sets it while preparing the message for delivery, after any configured plan delay. Neither timestamp marks a completed write to your individual socket.
Record a third timestamp as soon as your client receives a message, before parsing or processing it. Keep the client clock synchronized with NTP before comparing it with the server timestamps.
| Measurement | Calculation |
|---|---|
| Detection to publication | sent_time_us - detected_time_us |
| Publication to your client | received_time_us - sent_time_us |
| Detection to your client | received_time_us - detected_time_us |
Publication to your client includes server queue and socket-write time, transport, client scheduling, and clock offset. On Fast, it also includes the configured plan delay. It does not isolate network latency. On Full, the configured plan delay is included in detection to publication instead.
Compare many events on the same endpoint, plan, and region. Report p50, p95, and p99 separately, with the event count and observation window. Use detection to your client to compare total observed delivery time across endpoints. Keep missing events and disconnects separate from latency statistics. These are wall-clock estimates, and clock error can produce misleading or negative results.
Record receipt time
ws.on("message", (data) => {
const receivedTimeUs = Math.round(
(performance.timeOrigin + performance.now()) * 1000
);
const event = JSON.parse(data.toString());
if (!("sent_time_us" in event)) return;
console.log({
detectionToPublicationUs: event.sent_time_us - event.detected_time_us,
publicationToClientUs: receivedTimeUs - event.sent_time_us,
detectionToClientUs: receivedTimeUs - event.detected_time_us,
});
});
In the Python client, add the timestamp immediately after recv_data returns:
opcode, raw = ws.recv_data(control_frame=True)
received_time_us = time.time_ns() // 1000
Keep the close-frame and control-message handling. Replace print(message) in the announcement / tweet branch with:
print({
"detection_to_publication_us": message["sent_time_us"] - message["detected_time_us"],
"publication_to_client_us": received_time_us - message["sent_time_us"],
"detection_to_client_us": received_time_us - message["detected_time_us"],
})
The client already imports time. Control messages do not contain event timestamps.