Leap seconds#
Why#
A Stratum 1 server doesn’t just track a clock that ticks steadily forward: it also needs to know that, once every few years, an extra second gets inserted into the civil calendar. Ignoring this mechanism, or handling it with a stale data file, produces a full one-second offset, an eternity at the precision level this project targets (around a microsecond). This chapter explains where leap seconds come from and how this project automates handling them robustly.
Where leap seconds come from#
Three time scales coexist, with different progression rules:
| Scale | Basis | Behavior |
|---|---|---|
| TAI (International Atomic Time) | Weighted average of over 400 cesium atomic clocks, spread across ~80 national laboratories | Monotonic, strictly increasing, never jumps |
| UT1 (Astronomical Universal Time) | The Earth’s actual rotation, measured via interferometry on quasars | Gradually slows down (lunar tidal effect), with irregular variations |
| UTC (Coordinated Universal Time) | The legal civil time scale | Stays close to UT1 (within 0.9 s) while benefiting from TAI’s stability |
The Earth rotates slightly slower than it did when the second was first defined, a tiny discrepancy, but one that accumulates. For UTC (the legal time everyone uses) to never drift more than 0.9 second from the Earth’s actual rotation, the IERS (International Earth Rotation and Reference Systems Service) periodically inserts a leap second, always on June 30 or December 31 at 23:59:59 UTC, when needed.
Since the last leap second (January 1, 2017), the gap TAI − UTC = 37 seconds. This value only changes in whole-second steps, announced in advance by the IERS via its “Bulletin C”.
A major decision for the future (CGPM resolution 4, 2022): starting in 2035, the UTC-UT1 tolerance will be relaxed and leap seconds will be frozen at their then-current value, a paradigm shift after more than 50 years of leap seconds, driven by the difficulties these unpredictable jumps cause modern computing systems. See CGPM resolution 4.
How GNSS and chrony handle this gap#
GPS Time, like TAI, is continuous: it never undergoes a leap second. Currently, GPS Time = UTC + 18 seconds. The GNSS receiver gets the current UTC correction directly from the navigation message broadcast by the satellites (subframe 4 of the GPS message), refreshed several hours ahead of any potential leap second event.
On the server side, chrony needs its own source of information about
this gap, via a leap-seconds.list file published by IANA/IERS:
# In chrony.conf
leapseclist /etc/chrony/leap-seconds.listThis file includes, among other things, an expiration date, beyond which chrony can no longer guarantee the correction is up to date, and flags it.
Automated, validated updates#
cat > /usr/local/sbin/leap-seconds-update <<'EOF'
#!/bin/bash
# VALIDATED update of /etc/chrony/leap-seconds.list.
# Installs + restarts chrony ONLY if the content actually changed.
set -euo pipefail
URL="https://data.iana.org/time-zones/data/leap-seconds.list"
DST="/etc/chrony/leap-seconds.list"
NTP_EPOCH_OFFSET=2208988800 # 1900-01-01 -> 1970-01-01
log() { logger -t leap-seconds "$1"; }
fail() { log "ERROR: $1"; exit 1; }
TMP=$(mktemp /tmp/leap-seconds.XXXXXX)
trap 'rm -f "$TMP"' EXIT
curl -fsSL --max-time 60 "$URL" -o "$TMP" || fail "download failed"
# a. Structural validation (key lines present)
grep -q '^#\$' "$TMP" || fail "missing #\$ line (last update)"
grep -q '^#@' "$TMP" || fail "missing #@ line (expiration)"
grep -q '^#h' "$TMP" || fail "missing #h line (hash)"
# b. The downloaded file isn't itself already expired
EXPIRY_NTP=$(awk '/^#@/ {print $2; exit}' "$TMP")
EXPIRY_UNIX=$((EXPIRY_NTP - NTP_EPOCH_OFFSET))
[ "$EXPIRY_UNIX" -gt "$(date +%s)" ] || fail "downloaded file already expired"
# c. Integrity: embedded SHA-1 hash recomputed and compared
STORED=$(awk '/^#h/ { for (i=2; i<=NF; i++) printf "%08s", $i; exit }' "$TMP" | tr ' ' '0')
COMPUTED=$( {
awk '/^#\$/ {printf "%s", $2; exit}' "$TMP"
awk '/^#@/ {printf "%s", $2; exit}' "$TMP"
sed 's/#.*$//' "$TMP" | awk 'NF >= 2 {printf "%s%s", $1, $2}'
} | sha1sum | cut -d' ' -f1 )
[ "$STORED" = "$COMPUTED" ] || fail "invalid SHA-1 hash"
# Install only if content changed
if cmp -s "$TMP" "$DST"; then
log "no change (expires $(date -u -d "@$EXPIRY_UNIX" +%F))"
exit 0
fi
install -o root -g root -m 0644 "$TMP" "$DST"
log "new file installed, restarting chrony"
systemctl restart chrony # leapseclist is only read at startup
EOF
chmod +x /usr/local/sbin/leap-seconds-update
cat > /etc/cron.d/leap-seconds <<'EOF'
17 3 1 * * root /usr/local/sbin/leap-seconds-update
EOFThe 17 3 offset (3:17 AM, rather than a round hour like midnight or
3:00) avoids lining this job up with other cron jobs scheduled on round
times, a common convention for spreading out load rather than an
arbitrary value. The first day of the month (1) gives a monthly
cadence, ample for a file that changes at most twice a year.
Pitfalls to avoid#
A plain
curlwithout validation can overwrite a valid file with an error page. If the remote server is temporarily unavailable or returns a CDN error page, a naive script (curl -o /etc/chrony/leap-seconds.list ...) silently overwrites a working file with invalid content, chrony will then fail to parse the file on its next restart, potentially long after the download incident, making diagnosis hard. Always download to a temp file, validate before installing, and only replace the live file after full validation.
- Restarting chrony on every cron run, even without a change. The
leap-seconds.listfile changes at most twice a year, often less. An unconditionalsystemctl restart chronyon every periodic run needlessly breaks the ongoing clock discipline loop (see the Chrony chapter), only restarting when the content actually changed (cmp -s) is the right practice. - Running the update too rarely. An update frequency of “twice a year, hoping it lines up” means a one-off download failure (temporary network outage) isn’t retried for another six months, well past the file’s potential expiration. A monthly frequency combined with no-op detection (so no cost on a retried failed download) is a much better trade-off.
- Forgetting that
leapseclistis only read at chronyd startup. Unlike TLS certificate reloading (NGINX/NTS chapter), which can happen live viaSIGHUP, a newleap-seconds.listfile requires a full daemon restart to take effect, hence the value of only doing so when truly necessary (previous point).
Verify#
chronyc tracking | grep "Leap status"
# Should return "Normal" outside the 24h preceding a leap second
# Expiration date of the currently loaded file
awk '/^#@/ {print $2}' /etc/chrony/leap-seconds.list
# Manual test of the script (should log "no change" if already up to date)
/usr/local/sbin/leap-seconds-update
journalctl -t leap-seconds -n 5📚 Going further
- The rules governing UTC and leap seconds are defined by the ITU (International Telecommunication Union), recommendation ITU-R TF.460-6.
- Official leap second announcements are published by the IERS via its Bulletin C, available at iers.org.
- The decision to freeze leap seconds starting in 2035 was adopted by the General Conference on Weights and Measures (CGPM), resolution 4 of 2022, a historic change after more than 50 years of practice.
- The format and computation of the GPS Time → UTC correction (ΔtLS parameters) are specified in IS-GPS-200, subframe 4, page 18 of the navigation message.