NTP can be a surprisingly ineffective timekeeper if you’re not careful about its internal workings.
Let’s see what a healthy NTP exchange looks like. Imagine we’re on a client machine, and we want to sync with a server at ntp.example.com.
First, the client sends a request to the server. This is an NTP "mode 3" (client) packet.
00:00:00.000000 192.168.1.100 > 192.168.1.1 NTPv3, Client, length 48
Flags [none], Stratum 0 (unspecified), poll 4, precision 24
Root Delay 0.000000, Root Jitter 0.000000
Reference Clock Identifier (none)
Reference Timestamp 0.000000
Origin Timestamp 0.000000
Receive Timestamp 0.000000
Transmit Timestamp 0.000000
Immediately after, the client records its transmit timestamp (which is the current time on the client, 0.000000 in this raw capture snippet, but would be the actual client time). This is crucial.
The server receives this packet. It notes the receive timestamp on its end (let’s call this T_server_rx). Then, it processes the request and prepares a reply.
The server generates its reply packet, which includes several timestamps:
- Reference Timestamp: The time the server last synchronized with its upstream source.
- Originate Timestamp: The client’s transmit timestamp from its original request packet.
- Receive Timestamp: The time the server received the client’s request (
T_server_rx). - Transmit Timestamp: The time the server sends its reply packet (let’s call this
T_server_tx).
The server sends this reply back to the client.
00:00:00.050000 192.168.1.1 > 192.168.1.100 NTPv3, Server, length 48
Flags [none], Stratum 2 (primary server), poll 4, precision -18
Root Delay 0.000000, Root Jitter 0.000000
Reference Clock Identifier (GPS) 123.45.67.89
Reference Timestamp 3876543210.123456789
Originate Timestamp 0.000000 <-- This is the client's T_client_tx
Receive Timestamp 3876543210.150000000 <-- This is the server's T_server_rx
Transmit Timestamp 3876543210.150000001 <-- This is the server's T_server_tx
When the client receives the server’s reply, it records the receive timestamp on its end (let’s call this T_client_rx). Now, the client has all the pieces of the puzzle:
T_client_tx: The client’s timestamp when it sent the request.T_server_rx: The server’s timestamp when it received the request.T_server_tx: The server’s timestamp when it sent the reply.T_client_rx: The client’s timestamp when it received the reply.
The client uses these four timestamps to calculate the offset and delay.
The network delay (delta) is calculated as:
delta = (T_client_rx - T_client_tx) - (T_server_tx - T_server_rx)
The offset (theta) is calculated as:
theta = ((T_server_rx - T_client_tx) + (T_server_tx - T_client_rx)) / 2
Essentially, theta is the average of the time it took the packet to go from client to server and back, and delta is the asymmetry in that round trip. The client then adjusts its clock to match the server’s time, taking both theta and delta into account.
The most surprising true thing about NTP analysis is that the transmit timestamp of the client’s initial request is what the server uses to mark the originate timestamp in its reply. This means the client’s clock at the moment of sending the request is the reference point for the server’s calculation, not just the time the server receives it.
Here’s a snapshot of a Wireshark capture showing these timestamps in action. We’ll filter for NTP traffic.
frame.number == 10
...
0.000000 192.168.1.100 -> 192.168.1.1 NTPv3, Client, length 48
Transmit Timestamp: 3876543210.123456789 <-- T_client_tx
frame.number == 12
...
0.050000 192.168.1.1 -> 192.168.1.100 NTPv3, Server, length 48
Originate Timestamp: 3876543210.123456789 <-- T_client_tx from frame 10
Receive Timestamp: 3876543210.150000000 <-- T_server_rx
Transmit Timestamp: 3876543210.150000001 <-- T_server_tx
And on the client side, when it receives frame 12:
frame.number == 15
...
0.050100 192.168.1.100 <- 192.168.1.1 NTPv3, Server, length 48
Receive Timestamp: 3876543210.150100000 <-- T_client_rx
Using these values:
T_client_tx=3876543210.123456789T_server_rx=3876543210.150000000T_server_tx=3876543210.150000001T_client_rx=3876543210.150100000
delta = (0.150100000 - 0.123456789) - (0.150000001 - 0.150000000)
delta = 0.026643211 - 0.000000001 = 0.026643210 seconds (round trip delay)
theta = ((0.150000000 - 0.123456789) + (0.150000001 - 0.150100000)) / 2
theta = (0.026543211 + -0.000099999) / 2
theta = 0.026443212 / 2 = 0.013221606 seconds (offset)
The client’s clock is ahead by approximately 13.2 milliseconds.
The key to accurate NTP is minimizing the delay and jitter between the client and server. This means ensuring a stable network path and avoiding overloaded network devices.
The next thing you’ll want to understand is how NTP handles different "stratum" levels and what happens when your server is several hops away from a true time source.