Time, Clocks, and Timers

Updated

September 8, 2026

Time, Clocks, and Timers

Overview

Systems programs schedule work, measure latency, and enforce deadlines. Go’s time package is enough for almost all of this—if you know wall clock vs monotonic behavior and avoid classic timer leaks.

Wall clock vs monotonic

t1 := time.Now()
// ... work ...
elapsed := time.Since(t1) // uses monotonic reading when possible

time.Now() carries both wall and monotonic components on modern Go. Subtraction (Since, Sub) prefers monotonic—so NTP steps don’t invent negative durations mid-measurement.

Need Use
Logs, certs, file mtimes Wall clock (Format, Truncate)
Timeouts, RTT, deadlines Monotonic-friendly (Since, Until, context + timer)
“Run at 03:00 wall” Wall clock + recompute after sleep

Never store only UnixNano() for elapsed timing across clock steps if you care about accuracy—prefer time.Time from Now().

Sleep and timers

time.Sleep(100 * time.Millisecond) // blocks goroutine

// prefer for cancelable wait:
t := time.NewTimer(100 * time.Millisecond)
defer t.Stop()
select {
case <-ctx.Done():
    return ctx.Err()
case <-t.C:
    // fired
}

Stop and drain

if !t.Stop() {
    select {
    case <-t.C:
    default:
    }
}

Failing to stop/drain can wake a select later and cause subtle double-fire bugs.

Ticker

tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
    select {
    case <-ctx.Done():
        return
    case <-tk.C:
        // periodic work — keep it shorter than period
    }
}

Slow handlers + ticker = tick pile-up (channel buffer size 1 drops intermediate ticks in recent Go for Ticker—still don’t do multi-second work on every tick without backpressure).

Deadlines with context

ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
// pass ctx to Dial, Query, HTTP

Compose: signal cancel + timeout cancel (NotifyContext outer, timeout inner or vice versa).

Parsing and formatting

t, err := time.Parse(time.RFC3339, "2026-08-04T12:00:00Z")
s := t.UTC().Format(time.RFC3339Nano)

For CLI flags: flag.Duration already parses 300ms, 2s, 1h.

File mtimes and caching

st, err := os.Stat(path)
mod := st.ModTime()
if mod.After(lastLoad) {
    // reload
}

Clock skew across NFS nodes can make mtime comparisons surprising—prefer content hashes for critical caches.

Minimal tool: deadline

// run command with max wall time
ctx, cancel := context.WithTimeout(context.Background(), *max)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
err := cmd.Run()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
    return fmt.Errorf("timeout after %s", *max)
}

Rules of thumb

Do Don’t
Since for latency Compare wall Unix() deltas for RTT
Stop timers you create Leak time.After in tight loops
Bound periodic work Do unbounded IO every tick without skip/logic
UTC in logs/storage Mix local TZ without documenting

Try next

  1. Measure a sleep with Since while changing system time (VM lab)—note stability.
  2. Rewrite a time.After loop to NewTimer + Reset.
  3. Build deadline 2s -- sleep 10 and confirm kill via CommandContext.