Skip to main content
⚡ Calmops

Monotonic Clock vs Wall Clock in Go: Time Measurement Best Practices

Introduction

Go’s time package automatically handles two types of clocks: the wall clock (real-world time) and the monotonic clock (elapsed time). Understanding the difference is essential for writing correct timing code — especially for benchmarks, timeouts, and rate limiters.

Wall Clock vs Monotonic Clock

Wall Clock

The wall clock is the “real” time — what you’d see on a clock on the wall. It’s synchronized with NTP (Network Time Protocol) and can jump forward or backward:

  • Daylight saving time changes
  • NTP corrections
  • Manual system time adjustments
  • Leap seconds

Use for: Timestamps, scheduling events at specific times, logging.

Monotonic Clock

The monotonic clock only moves forward. It measures elapsed time since some arbitrary point (usually system boot). It’s immune to system time changes.

Use for: Measuring durations, timeouts, benchmarks, rate limiting.

How Go Handles Both

Since Go 1.9, time.Now() returns a Time value that contains both a wall clock reading and a monotonic clock reading:

t := time.Now()
// t contains: wall time (2026-03-30 10:00:00) + monotonic offset (e.g., 1234567890 ns since boot)

When you compute a duration using time.Since() or subtract two Time values, Go automatically uses the monotonic reading if both values have one:

start := time.Now()
// ... do work ...
elapsed := time.Since(start)  // uses monotonic clock — accurate even if wall clock changes

Practical Examples

Measuring Elapsed Time

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()

    // Simulate work
    time.Sleep(2 * time.Second)

    elapsed := time.Since(start)
    fmt.Printf("Elapsed: %v\n", elapsed)
    // => Elapsed: 2.000123456s
}

time.Since(start) is equivalent to time.Now().Sub(start) — both use the monotonic clock.

Benchmarking a Function

func benchmark(name string, fn func()) {
    start := time.Now()
    fn()
    elapsed := time.Since(start)
    fmt.Printf("%s took %v\n", name, elapsed)
}

benchmark("sort", func() {
    data := []int{5, 3, 1, 4, 2}
    sort.Ints(data)
})
// => sort took 1.234µs

Timeout with Context

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

select {
case result := <-doWork(ctx):
    fmt.Println("Got result:", result)
case <-ctx.Done():
    fmt.Println("Timed out:", ctx.Err())
}

Rate Limiter

type RateLimiter struct {
    rate     time.Duration
    lastCall time.Time
}

func (r *RateLimiter) Allow() bool {
    now := time.Now()
    if now.Sub(r.lastCall) >= r.rate {
        r.lastCall = now
        return true
    }
    return false
}

limiter := &RateLimiter{rate: 100 * time.Millisecond}
for i := 0; i < 5; i++ {
    if limiter.Allow() {
        fmt.Println("Request allowed at", time.Now().Format("15:04:05.000"))
    }
    time.Sleep(50 * time.Millisecond)
}

When Wall Clock Matters

For scheduling events at specific times, you need the wall clock:

// Schedule something at a specific time
target := time.Date(2026, 3, 30, 15, 0, 0, 0, time.Local)
delay := time.Until(target)  // uses wall clock

if delay > 0 {
    time.AfterFunc(delay, func() {
        fmt.Println("It's 3 PM!")
    })
}

time.Until(t) is equivalent to t.Sub(time.Now()).

Stripping the Monotonic Reading

Sometimes you want to compare times without the monotonic component — for example, when storing times in a database and reading them back (the stored time won’t have a monotonic reading):

t1 := time.Now()
// Store t1 in database, read it back as t2
t2, _ := time.Parse(time.RFC3339, t1.Format(time.RFC3339))

// t1 has monotonic reading, t2 doesn't
// Comparing them directly may give unexpected results

// Strip monotonic reading for fair comparison
t1Stripped := t1.Round(0)  // Round(0) strips the monotonic reading
fmt.Println(t1Stripped.Equal(t2))  // now comparable

Time Formatting and Parsing

now := time.Now()

// Format
fmt.Println(now.Format("2006-01-02 15:04:05"))        // => 2026-03-30 10:00:00
fmt.Println(now.Format(time.RFC3339))                  // => 2026-03-30T10:00:00+08:00
fmt.Println(now.Format("Mon, 02 Jan 2006"))            // => Mon, 30 Mar 2026
fmt.Println(now.Unix())                                // Unix timestamp (seconds)
fmt.Println(now.UnixMilli())                           // milliseconds
fmt.Println(now.UnixNano())                            // nanoseconds

// Parse
t, err := time.Parse("2006-01-02", "2026-03-30")
t, err = time.Parse(time.RFC3339, "2026-03-30T10:00:00Z")

// Parse in local timezone
t, err = time.ParseInLocation("2006-01-02 15:04:05", "2026-03-30 10:00:00", time.Local)

Go’s reference time: Go uses Mon Jan 2 15:04:05 MST 2006 as the reference time for formatting. Each component has a specific value: month=1, day=2, hour=15, minute=4, second=5, year=2006.

Time Arithmetic

now := time.Now()

// Add duration
tomorrow := now.Add(24 * time.Hour)
nextWeek := now.Add(7 * 24 * time.Hour)
inFiveMinutes := now.Add(5 * time.Minute)

// Subtract duration
yesterday := now.Add(-24 * time.Hour)

// Difference between two times
diff := tomorrow.Sub(now)  // returns time.Duration
fmt.Println(diff.Hours())  // => 24

// Truncate to day boundary
today := now.Truncate(24 * time.Hour)

// Round to nearest hour
rounded := now.Round(time.Hour)

Common Pitfalls

Using Wall Clock for Elapsed Time

// BAD: wall clock can jump, giving wrong elapsed time
start := time.Now().Unix()
time.Sleep(time.Second)
elapsed := time.Now().Unix() - start  // could be wrong if clock changes

// GOOD: use time.Since which uses monotonic clock
start := time.Now()
time.Sleep(time.Second)
elapsed := time.Since(start)  // always correct

Comparing Times from Different Sources

// Times from time.Now() have monotonic readings
t1 := time.Now()

// Times parsed from strings do NOT have monotonic readings
t2, _ := time.Parse(time.RFC3339, "2026-03-30T10:00:00Z")

// Subtraction: t1.Sub(t2) uses wall clock (t2 has no monotonic)
// This is correct behavior, but be aware of it

Summary

Use Case Clock Type Go Function
Measure elapsed time Monotonic time.Since(start)
Benchmark code Monotonic time.Since(start)
Implement timeout Monotonic context.WithTimeout
Schedule at specific time Wall time.Until(target)
Log timestamps Wall time.Now().Format(...)
Store in database Wall time.Now() (strip monotonic with .Round(0))

Resources

Comments