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)) |
Comments