skip to content

Why is a latency measured with time.Since immune to wall-clock jumps, and what strips that protection?

level: middleimportance: should knowfreq 42%

answer

  1. a time.Time is not just an instant
  2. two clocks live in the same value
  3. subtraction prefers one of them
  4. anything calendar-shaped drops it
  5. UTC, Round and marshalling strip it

basics

~20 s

time.Now attaches a monotonic clock reading to the time.Time it returns, and time.Since subtracts those readings, so an NTP step cannot distort the result. Calls such as UTC, Local, Round and any serialization drop the reading, leaving wall-clock arithmetic.

solid answer

~40 s

A `time.Time` from `time.Now()` carries two things: a wall-clock instant and a monotonic clock reading that only means something inside this process. `t.Sub(u)`, and therefore `time.Since` and `time.Until`, use the monotonic readings whenever both operands have one, so a latency measured that way is unaffected by NTP steps, daylight-saving changes or an operator setting the clock mid-request. The reading is dropped by anything that reinterprets the value as a wall-clock time: `UTC`, `Local`, `In`, `Round`, `Truncate`, `AddDate`, and every marshalling method, since the reading is meaningless outside the process. `t.Round(0)` is the documented way to strip it deliberately. So a start time that has been normalised to UTC for a log line, or round-tripped through JSON or a database, silently degrades your timer to wall-clock subtraction.

code

go · 7 lines
go
start := time.Now()   // carries a monotonic reading
logged := start.UTC() // same instant, monotonic reading stripped

// ... handle the request ...

d1 := time.Since(start)  // monotonic subtraction: unaffected by clock changes
d2 := time.Since(logged) // wall-clock subtraction: an NTP step lands in this number

go deeper

for a junior

Know that time.Now returns a value carrying more than a calendar timestamp, and that time.Since is the right way to measure elapsed time rather than subtracting formatted values.

for a middle

Explain which operations consume the monotonic reading and which strip it, and be able to name at least UTC, Round and marshalling as the ways a timer silently degrades to wall-clock arithmetic.

for a senior

Recognise the symptom in production: negative or absurd latency buckets after an NTP correction, and a start time that has been normalised or round-tripped somewhere in the request struct.

for a principal

Set the rule for durations that cross process boundaries, where no monotonic guarantee exists, and decide whether cross-service timings are reported as measurements or as estimates with clamping.

## Two clocks in one value Go's `time.Time` is a struct, not just an instant. A value returned by `time.Now()` holds a **wall clock** reading — the calendar time a human recognises, which the operating system may adjust at any moment — and, optionally, a **monotonic clock** reading, a counter that only ever moves forward at a steady rate since some arbitrary point (usually process or machine start). The monotonic reading has no meaning outside the running process: it cannot be compared with a reading from another machine, or from the same machine after a restart. ## Which operations use which The rule is simple and worth memorising: **`t.Sub(u)` uses the monotonic readings if both `t` and `u` have one, and falls back to wall-clock subtraction otherwise.** `time.Since(t)` is defined as `time.Now().Sub(t)` and `time.Until(t)` as `t.Sub(time.Now())`, so both inherit that behaviour. Formatting, `Year`, `Hour`, `Format` and friends always use the wall clock, because those questions are only meaningful in calendar terms. That is why a request timer built on `start := time.Now()` and `time.Since(start)` is trustworthy. If NTP steps the machine's clock backwards by 200ms in the middle of a request, wall-clock subtraction would report a negative duration; the monotonic subtraction is unaffected. On a busy service, over enough requests, those distortions do appear, and a histogram containing a negative or hour-long bucket is very hard to explain after the fact. ## What strips the monotonic reading Anything that treats the value as a wall-clock time drops the monotonic reading from its result: - `t.UTC()`, `t.Local()`, `t.In(loc)` — they exist to reinterpret the wall clock, so they strip it. - `t.Round(d)`, `t.Truncate(d)`, `t.AddDate(y, m, d)` — wall-clock computations. - `t.MarshalJSON`, `t.MarshalText`, `t.MarshalBinary`, `t.GobEncode` — the reading cannot survive leaving the process, so it is simply omitted. - Anything read back from JSON, a database row or an RPC message: it never had a monotonic reading to begin with. `t.Add(d)` and `t.Sub(u)` keep the monotonic reading; `t.Round(0)` is the documented, deliberate way to strip it — useful when you are about to store or compare a timestamp and want to be explicit. The realistic failure is quiet. Someone adds a debug field to the request-scoped struct that holds the start time, normalises it with `.UTC()` so the log line is clean, and the timer keeps compiling and keeps producing plausible numbers — just wall-clock ones. ## The comparison trap Because the monotonic reading is part of the struct, `==` on two `time.Time` values compares the wall clock, the monotonic reading **and** the location pointer. Two values representing the same instant can therefore be unequal under `==`. Use `t.Equal(u)`, which compares instants, and use it for map keys only after deliberately stripping with `Round(0)`. ## Practical consequences for instrumentation - Take `start := time.Now()` and keep that exact value until you measure. Do not normalise it, do not round it, do not send it anywhere and read it back. - If you must log the request start as a timestamp, keep two fields: the raw `time.Time` for measuring and a formatted string for the log. - A duration that crosses a process boundary — a start time in an inbound header, for example — is inherently wall-clock arithmetic across two machines' clocks. It can be negative, and any code consuming it must tolerate that. - `time.Duration` is an `int64` count of nanoseconds. Compute the duration once and convert at the edge (`d.Seconds()`, `d.Milliseconds()`), rather than storing floats and subtracting them.

  • A start time arrives in a request header from another service. Can you subtract it safely?
    Not with the same guarantees. A parsed timestamp has no monotonic reading, so the subtraction is wall-clock arithmetic across two machines' clocks. Skew makes negative or absurdly large values realistic, so clamp them and treat the result as an estimate. Only measurements taken by one process from `time.Now()` to `time.Since` are monotonic.
  • Why prefer t.Equal(u) over t == u for time.Time values?
    `==` compares the whole struct: the wall clock, the monotonic reading and the `*Location` pointer. Two values for the same instant — one from `time.Now()`, one parsed or converted — can compare unequal. `Equal` compares the instants they represent, which is almost always the question you actually mean.
  • How do you deliberately drop the monotonic reading?
    `t = t.Round(0)`. It is documented as the canonical way to strip it, and it is worth doing explicitly before you store a `time.Time` in a long-lived struct, use it as a map key, or compare it with values that were parsed rather than measured.

saying these in an interview costs you the question

  • Claiming Go has only one clock and time.Now is pure wall time
  • Normalising the start time with UTC before measuring elapsed time
  • Comparing time.Time values with == and expecting instant equality
  • Computing latency from a timestamp parsed out of a header as if it were monotonic
  • Formatting both times and subtracting the seconds fields