Skip to content
Console

Time sync

/ntp returns the time the server received the request and the time it sent the response (Unix milliseconds, precise to microseconds). The server syncs with an upstream NTP server every 30 seconds, and the times it returns are already corrected. Record when you sent the request and when you received the response, and the NTP formulas give the local clock offset. This fits applications that need millisecond time (for example, comparing earthquake data timestamps).

No API Token is required right now.

GEThttps://api.lb.exptech.dev/ntp
Request
curl "https://api.lb.exptech.dev/ntp"
Response

Success (text/plain). The body is T3: the server time when the response was sent (the same value as X-NTP-T3), Unix milliseconds with 3 decimal places, and a trailing newline

1794198117123.457
Try it

The token is sent from your browser directly to the API.

The body is only T3. The rest of the timing is in response headers:

FieldTypeDescription
X-NTP-T2stringT2: the time the server received the request, Unix milliseconds with 3 decimal places (for example 1794198117123.412).
X-NTP-T3stringT3: the time the server sent the response. Same format as T2.
X-NTP-StatusstringThe server's own sync status: synced (synchronized), unsynced (not yet synchronized since startup), or error (repeated sync failures). When the status is not synced, T2 and T3 may be uncorrected.

Let:

  • T1: the time the client sent the request (local clock)
  • T2: the time the server received the request (X-NTP-T2)
  • T3: the time the server sent the response (X-NTP-T3)
  • T4: the time the client received the response (local clock)

All four values are in milliseconds.

offset = ((T2 − T1) + (T3 − T4)) / 2
delay = (T4 − T1) − (T3 − T2)
standard time ≈ local time + offset
  • A positive offset means the local clock is behind the server.
  • delay is the network round-trip time (minus server processing time). When the outbound and return paths are asymmetric, the error in offset is at most delay / 2, so the sample with the smaller delay is the more accurate one.

Recommended procedure:

  1. Take 5–10 samples in a row, and drop the first one (it includes connection setup, so the two directions are the most asymmetric).
  2. Drop samples whose X-NTP-Status is not synced.
  3. Take the offset of the sample with the smallest delay.
  4. Measure T1 and T4 with a monotonic clock (for example performance.timeOrigin + performance.now()), so a system-time change during the measurement does not affect it.

Clock sync using the procedure above: sample in a row, drop the first sample, and take the offset of the sample with the smallest delay.

Node.js 18 or later. On the server you can read the headers directly:

// ntp.mjs — run: node ntp.mjs
const BASE = "https://api.lb.exptech.dev";
const now = () => performance.timeOrigin + performance.now();
async function sample() {
const t1 = now();
const res = await fetch(`${BASE}/ntp`, { cache: "no-store" });
await res.text();
const t4 = now();
const t2 = Number(res.headers.get("X-NTP-T2"));
const t3 = Number(res.headers.get("X-NTP-T3"));
return {
status: res.headers.get("X-NTP-Status"),
offset: (t2 - t1 + (t3 - t4)) / 2,
delay: t4 - t1 - (t3 - t2),
};
}
async function syncClock(count = 8) {
await sample(); // The first sample includes connection setup. Drop it.
const samples = [];
for (let i = 0; i < count; i++) {
const s = await sample();
if (s.status === "synced") samples.push(s);
}
if (samples.length === 0) throw new Error("no usable samples");
return samples.reduce((best, s) => (s.delay < best.delay ? s : best));
}
const { offset, delay } = await syncClock();
console.log(`local clock offset ${offset.toFixed(1)} ms (round trip ${delay.toFixed(1)} ms)`);
console.log("corrected time", new Date(Date.now() + offset).toISOString());

Server processing time is usually far under 1 millisecond, so you can treat T2 ≈ T3. The formulas simplify to:

offset ≈ T3 − (T1 + T4) / 2
delay ≈ T4 − T1
const now = () => performance.timeOrigin + performance.now();
async function sample() {
const t1 = now();
const res = await fetch("https://api.lb.exptech.dev/ntp", { cache: "no-store" });
const t3 = Number(await res.text());
const t4 = now();
return { offset: t3 - (t1 + t4) / 2, delay: t4 - t1 };
}
async function syncClock(count = 8) {
await sample(); // Drop the first sample
let best = null;
for (let i = 0; i < count; i++) {
const s = await sample();
if (!best || s.delay < best.delay) best = s;
}
return best;
}
const { offset, delay } = await syncClock();
console.log(`local clock offset ${offset.toFixed(1)} ms (round trip ${delay.toFixed(1)} ms)`);

This approach cannot drop samples whose X-NTP-Status is not synced.

  • The time unit is milliseconds (not seconds), with 3 decimal places.
  • Send cache: "no-store" (or Cache-Control: no-cache) so you do not get a cached old time.
  • You do not need to call it often. After one sync, compute time from the local monotonic clock plus offset. Resync every few minutes.
  • This is an estimate over HTTP. The error is at most that sample’s delay / 2. A slower network, or a more asymmetric outbound and return path, means a larger error. For higher accuracy, use the operating system’s NTP client.
  • You can also pick a specific node’s Base URL (https://api.lb-tpe1.exptech.dev or https://api.lb-khh1.exptech.dev). Use the same node for one sync.