When to Use
Use this skill when a co-located feed handler records tick arrival times that must
reflect when the packet hit the wire, not when Python got around to reading it.
An application-level time.time() reading includes NIC-to-kernel DMA, softirq
scheduling, socket-buffer queueing and the interpreter's own context switches.
Linux exposes earlier capture points through SO_TIMESTAMPING, and this skill
covers decoding them correctly.
The one thing this skill exists to get right is which capture point you actually
received. Three different SOL_SOCKET control messages carry a timestamp, two of
them are byte-identical in size on a 64-bit host, and only one of them can ever
contain a hardware timestamp:
cmsg_type |
Enabled by | Payload | Size (LP64 / ILP32) | Capture point |
|---|---|---|---|---|
SCM_TIMESTAMP (29 / 63) |
SO_TIMESTAMP |
struct __kernel_old_timeval |
16 / 8 | Kernel receive path, microseconds |
SCM_TIMESTAMPNS (35 / 64) |
SO_TIMESTAMPNS |
struct timespec |
16 / 8 | Kernel receive path, nanoseconds |
SCM_TIMESTAMPING (37 / 65) |
SO_TIMESTAMPING |
struct scm_timestamping (timespec[3]) |
48 / 24 | ts[0] software, ts[1] deprecated, ts[2] hardware |
Classifying these by buffer length instead of cmsg_type is the classic failure:
a 16-byte payload is a software timestamp either way, and a 48-byte payload decoded
from offset 0 yields ts[0] — the kernel software timestamp — which then gets
recorded and reported as a wire timestamp.
When NOT to Use
- On any non-Linux host.
SO_TIMESTAMPINGis a Linux socket option. There is no portable equivalent, and the engine returnsFalsefromenable_nic_timestampingrather than pretending otherwise. - To subtract a hardware timestamp from a system-clock reading, without a
disciplined PHC. The kernel does not convert hardware timestamps to system time;
ts[2]is read from the adapter's PTP hardware clock, an independent clock. Untilphc2sys/sfptpdties it toCLOCK_REALTIME,T_app − T_hwis a clock offset plus a capture delay, and routinely goes negative. Seeclock-synchronization-ptp-for-trading-hosts. - As evidence of MiFID II RTS 25 clock compliance. Decoding a timestamp is not
auditing one. Use
hardware-timestamping-vs-software-timestamping-accuracyfor the divergence/granularity audit and the documented timestamping point. - As a substitute for a latency budget. Wire-to-decision decomposition across the
whole path is
tick-to-trade-latency-measurementandstrategy-latency-budget-decomposition. - With a kernel-bypass stack that never touches a Linux socket. Onload's
onload_timestampingand equivalent bypass APIs deliver timestamps through the vendor library, not throughrecvmsgancillary data; the layout facts here still apply, the socket plumbing does not.
Prerequisites
- Linux host with an adapter whose driver supports receive hardware timestamping.
Verify before trusting any
HARDWARE_NIClabel:ethtool -T <iface>must listhardware-receive/SOF_TIMESTAMPING_RX_HARDWAREin its capabilities. A driver without it still returns a populatedSCM_TIMESTAMPINGmessage — the hardware slot is simply zero. ptp4ldisciplining the PHC to a grandmaster, andphc2sys(orsfptpd) relating the PHC to the system clock, if any cross-layer subtraction is to mean anything.socket.recvmsgwith an ancillary buffer sized viasocket.CMSG_SPACE(48)—struct scm_timestampingis 48 bytes on LP64, and an undersizedancbufsizetruncates the control message rather than failing loudly.- CPython's
socketmodule exports none ofSO_TIMESTAMPING,SCM_TIMESTAMPINGor theSOF_TIMESTAMPING_*flags. The numeric constants must be supplied by the caller;hasattr(socket, "SO_TIMESTAMPNS")isFalseon every platform and must never be used to gate activation.
Workflow
-
Confirm the adapter can do it before enabling anything. Run
ethtool -T <iface>. Decision point: ifhardware-receiveis absent, do not ship a config that claims hardware timestamps — either fix the driver/NIC or setuse_hardware_timestamping=Falseand record kernel software timestamps under their real name. -
Enable
SO_TIMESTAMPING, notSO_TIMESTAMPNS.engine = NICHardwareTimestamperEngine(use_hardware_timestamping=True) if not engine.enable_nic_timestamping(sock): raise RuntimeError("hardware timestamping unavailable — do not start the feed")The engine sets
SOF_TIMESTAMPING_RX_HARDWARE | SOF_TIMESTAMPING_RAW_HARDWARE(0x44). Decision point: never additionally enableSO_TIMESTAMPorSO_TIMESTAMPNSon the same socket — withSOF_TIMESTAMPING_SOFTWAREset, the kernel fabricates a substitute software timestamp intots[0]when a real one is missing, and it is indistinguishable from a genuine capture. -
Receive with an adequately sized control buffer.
payload, ancdata, flags, addr = sock.recvmsg(65535, socket.CMSG_SPACE(48)) -
Decode by
cmsg_type, and take the hardware timestamp fromts[2].pkt = engine.process_packet_with_nic_timestamp(payload, ancdata, time.time_ns())Precedence is hardware (
ts[2]) → kernel nanosecond → kernel microsecond → application clock. All control messages are scanned before one is chosen;recvmsgdoes not guarantee the hardware message arrives first. An all-zerots[2]means the adapter did not stamp this packet, not that it stamped it at the epoch. -
Treat degradation as an incident, not a log line. Decision point: if
pkt.degradedis set, hardware timestamping was required and did not arrive — the tick stream's stated accuracy is no longer true. Alert; do not silently continue writing ticks that claim wire accuracy. -
Read
capture_delay_nsas signed, and know which timebase it crosses. Whenpkt.cross_timebase_comparisonisTruethe figure mixes the PHC andCLOCK_REALTIME. A negative value is not a fast packet — it is an undisciplined PHC. Fix the clock before reading anything else from that batch.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Classifying the timestamp by control-buffer length. On LP64 both
SCM_TIMESTAMPNS(timespec) andSCM_TIMESTAMP(timeval) are exactly 16 bytes. Decoding atimevalas atimespecreadstv_usecas nanoseconds and under-reports the sub-second component by a factor of 1000 — half a second becomes half a millisecond — while a size test also labels both as "hardware". - Reading
ts[0]out ofstruct scm_timestampingand calling it hardware.ts[0]is the software timestamp; the hardware one ists[2].ts[1]held hardware-converted-to-system-time and is deprecated — do not resurrect it. - Gating on
hasattr(socket, "SO_TIMESTAMPNS"). CPython never defines it, so the guard is always false and timestamping is silently never enabled. The failure looks exactly like a working system that simply never sees hardware timestamps. - Enabling
SO_TIMESTAMP/SO_TIMESTAMPNSalongsideSO_TIMESTAMPING. WithSOF_TIMESTAMPING_SOFTWARErequested, the kernel generates a false software timestamp ints[0]duringrecvmsg()when a real one is missing. - Clamping the capture delay at zero.
max(0.0, T_app − T_hw)turns the single clearest symptom of an undisciplined PHC into a healthy-looking0.0and hides the PTP fault the rest of this skill is trying to surface. - Reading only the negative case as a clock fault. A PHC that was never set counts
from zero at boot, so
T_app − T_hwcomes back as a positive delay of roughly the current epoch — tens of years — rather than a negative one. Both directions are the same fault. Sanity-check the magnitude ofcapture_delay_nswhenevercross_timebase_comparisonisTrue, using a bound derived from your own measured capture path rather than a number copied from a datasheet. - Carrying a nanosecond epoch in a float.
int(time.time() * 1e9)at a ~1.78e18 ns epoch has 256 ns of binary64 spacing — coarser than the effect being measured. Usetime.time_ns()and integers end to end. - Trusting a populated
SCM_TIMESTAMPINGmessage as proof of hardware capture. The message is delivered whether or not the adapter stamped the packet. Confirm withethtool -Tand check thatts[2]is non-zero. - Sizing
ancbufsizefor a 16-byte timespec.struct scm_timestampingneedsCMSG_SPACE(48)on LP64; a smaller buffer truncates the control message and the hardware slot is the part that gets cut. - Assuming the constants are portable across architectures.
SO_TIMESTAMPING_OLDis 37 underasm-generic(x86-64, arm64, mips, alpha) but0x0023on sparc and0x4020on parisc. Pass the overrides rather than hard-coding.
Verification
- Build a
struct scm_timestampingwithts[0]at +900,000 ns andts[2]at +400,000 ns and assert the selectedtimestamp_nsis the +400,000 value withsource == HARDWARE_NIC. A decoder reading offset 0 returns +900,000 here. - Feed a 16-byte
SCM_TIMESTAMPNSpayload and assertsource == KERNEL_SOFTWARE, notHARDWARE_NIC. - Feed a 16-byte
SCM_TIMESTAMPpayload withtv_usec = 500_000and assert the decoded value isbase + 500_000_000ns, notbase + 500_000ns. - Present
SCM_TIMESTAMPNSahead ofSCM_TIMESTAMPINGin the ancillary list and assert the hardware timestamp still wins. - Set
ts[2]250 µs ahead of the application timestamp and assertcapture_delay_ns == -250_000andcross_timebase_comparison is True— the value must be signed, not clamped. - Assert an all-zero
ts[2]yieldsKERNEL_SOFTWAREwithdegraded is True, and a 32-byte (truncated)SCM_TIMESTAMPINGpayload yieldsAPPLICATION_FALLBACKrather than a misdecoded number. - Assert
tv_nsec == 1_000_000_000is rejected, a float epoch raisesTypeError, and a negative epoch raisesValueError. - Assert
timestamping_flags()is68(1<<2 | 1<<6) when hardware is required and92when software fallback is allowed, and that the default option number is 37, not 35. - Run
python -m unittest discover -s skills/network-interface-level-tick-timestamping/scripts.