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).
Authentication
Section titled “Authentication”No API Token is required right now.
Get the server time
Section titled “Get the server time”https://api.lb.exptech.dev/ntp curl "https://api.lb.exptech.dev/ntp"curl "https://api.lb-tpe1.exptech.dev/ntp"curl "https://api.lb-khh1.exptech.dev/ntp"const res = await fetch("https://api.lb.exptech.dev/ntp");if (!res.ok) throw new Error(`HTTP ${res.status}`);console.log(await res.text());const res = await fetch("https://api.lb-tpe1.exptech.dev/ntp");if (!res.ok) throw new Error(`HTTP ${res.status}`);console.log(await res.text());const res = await fetch("https://api.lb-khh1.exptech.dev/ntp");if (!res.ok) throw new Error(`HTTP ${res.status}`);console.log(await res.text());import requests
res = requests.get("https://api.lb.exptech.dev/ntp", timeout=10)res.raise_for_status()print(res.text)import requests
res = requests.get("https://api.lb-tpe1.exptech.dev/ntp", timeout=10)res.raise_for_status()print(res.text)import requests
res = requests.get("https://api.lb-khh1.exptech.dev/ntp", timeout=10)res.raise_for_status()print(res.text)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.457Try it
Response fields
Section titled “Response fields”The body is only T3. The rest of the timing is in response headers:
| Field | Type | Description |
|---|---|---|
X-NTP-T2 | string | T2: the time the server received the request, Unix milliseconds with 3 decimal places (for example 1794198117123.412). |
X-NTP-T3 | string | T3: the time the server sent the response. Same format as T2. |
X-NTP-Status | string | The 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. |
Calculate clock offset
Section titled “Calculate clock offset”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)) / 2delay = (T4 − T1) − (T3 − T2)
standard time ≈ local time + offset- A positive
offsetmeans the local clock is behind the server. delayis the network round-trip time (minus server processing time). When the outbound and return paths are asymmetric, the error inoffsetis at mostdelay / 2, so the sample with the smallerdelayis the more accurate one.
Recommended procedure:
- Take 5–10 samples in a row, and drop the first one (it includes connection setup, so the two directions are the most asymmetric).
- Drop samples whose
X-NTP-Statusis notsynced. - Take the
offsetof the sample with the smallestdelay. - 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.
Advanced example
Section titled “Advanced example”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
Section titled “Node.js”Node.js 18 or later. On the server you can read the headers directly:
// ntp.mjs — run: node ntp.mjsconst 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());Browser
Section titled “Browser”Server processing time is usually far under 1 millisecond, so you can treat T2 ≈ T3. The formulas simplify to:
offset ≈ T3 − (T1 + T4) / 2delay ≈ T4 − T1const 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"(orCache-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.devorhttps://api.lb-khh1.exptech.dev). Use the same node for one sync.
