The difference between wall clocks and monotonic clocks
When measuring time in software systems, not all clocks are created equal. Understanding the fundamental differences between wall clocks and monotonic clocks can save you from insidious bugs that only manifest under specific timing conditions.
A wall clock, as the name suggests, is like the clock hanging on your wall. It tells you the current time of day – hours, minutes, seconds. In computing terms, wall clocks report the current time based on the system clock, which is synchronized with external time sources like NTP servers. Wall clocks are what you use when you need to know "what time is it right now?"
However, wall clocks have a critical weakness: they aren't guaranteed to move forward uniformly. The system time can jump forward or backward for various reasons:
- Time synchronization with NTP can cause small adjustments
- Manual time changes by system administrators
- Daylight saving time transitions
- Leap seconds
- Virtual machine migrations or suspensions
This non-uniformity creates a problem. Imagine you're trying to measure how long an operation takes:
start := time.Now() // Using wall clock
// ... operation ...
elapsed := time.Now().Sub(start)
If the system clock is adjusted backwards during your operation, elapsed
might be negative! Or if adjusted forward, your operation would appear to take longer than it actually did.
This is where monotonic clocks come in. A monotonic clock is designed specifically for measuring elapsed time, not telling the current time. Its key property is that it always moves forward at a uniform rate. It doesn't care about NTP adjustments, daylight saving time, or any other external time changes. Monotonic clocks provide a reliable time source for calculating durations.
Think of it this way:
- Wall clock: "It's 3:42 PM right now"
- Monotonic clock: "It's been 7,842,391 milliseconds since the system booted"
The monotonic clock doesn't tell you what time it is - it only tells you how much time has passed since some arbitrary starting point (often system boot). This makes it perfect for performance measurements, timeouts, and any other duration-related calculations where consistency matters more than actual time of day.
Most operating systems provide both types of clocks. In Linux, for instance, CLOCK_REALTIME
gives you the wall clock time, while CLOCK_MONOTONIC
provides a monotonic time source. The difference is subtle but crucial when building reliable systems that need to measure durations accurately.
How Go integrates monotonic time into time.Now()
Go's approach to handling time is particularly elegant, especially in how it deals with the wall clock vs. monotonic clock dilemma. Rather than forcing developers to choose between the two clock types explicitly, Go ingeniously combines them into a single value returned by time.Now()
.
When you call time.Now()
in Go, you don't just get a wall clock reading - you get a composite value that contains both wall clock time and a monotonic clock reading. This design decision makes it much easier to write correct time-measuring code without having to think about which clock to use in different scenarios.
Let's look at how this works under the hood:
t := time.Now()
The variable t
now contains both:
- The wall clock time (which you can format, convert to different time zones, etc.)
- An embedded monotonic clock reading (which is used for duration calculations)
This dual nature is invisible in daily use but extremely powerful. The genius of Go's approach is that the right clock is automatically used for the right purpose:
- When you display the time or convert it to a string, the wall clock component is used
- When you calculate a duration between two time instances, the monotonic component is used (if available)
The implementation relies on a clever bit hack. In the internal representation, Go stores the monotonic reading in the lower bits of the wall time's nanosecond field, which normally has more precision than needed. This allows the Time
type to carry both clock readings while maintaining backward compatibility with code that only expects wall time.
Here's an example of how seamlessly this works:
start := time.Now()
time.Sleep(100 * time.Millisecond)
elapsed := time.Now().Sub(start)
fmt.Println(elapsed) // Reliably prints ~100ms
Even if the system clock is changed between the two Now()
calls, the elapsed time will still be accurately reported because the duration calculation uses the monotonic clock component.
This integration means that as a Go developer, you rarely need to think about which clock to use - the language makes the right choice for you. The monotonic clock is automatically leveraged for measuring durations, while the wall clock is used for displaying the current time or calculating calendar-based time differences.
The one caveat is that the monotonic reading is only preserved as long as the Time
value stays within the same Go process. If you serialize a Time
value (to JSON, for example) and then deserialize it, the monotonic component is lost, and you're left with only the wall clock time.
Ensuring reliable time measurement with Sub()
, Since()
, and Until()
When it comes to measuring elapsed time in Go, the standard library provides several methods that leverage the monotonic clock component automatically. Understanding these methods is critical for writing code that accurately measures durations regardless of system clock changes.
The main functions you'll use for duration calculations are Sub()
, Since()
, and Until()
. Each has a specific purpose, but they all share a common trait: they automatically use the monotonic clock component when available.
The Sub()
method
The Sub()
method is the foundation for duration calculations in Go:
func (t Time) Sub(u Time) Duration
When you call t.Sub(u)
, Go compares the two time values and returns the duration between them. The magic happens in how this comparison is performed:
start := time.Now()
// ... some work ...
duration := time.Now().Sub(start)
If both time values have a monotonic clock reading (which they will if created with time.Now()
in the same process), Sub()
will use those readings to calculate the duration. This ensures that system clock adjustments don't affect your measurements.
The code is remarkably simple but incredibly reliable. Here's a practical example where this matters:
func measureResponseTime(url string) time.Duration {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
return 0
}
defer resp.Body.Close()
return time.Now().Sub(start)
}
Even if NTP adjusts the system clock during the HTTP request, the reported response time will still be accurate.
The Since()
helper function
The Since()
function is a convenient wrapper around time.Now().Sub()
:
func Since(t Time) Duration
It calculates the time elapsed since a past time t
. This is so common that having a dedicated function makes the code more readable:
start := time.Now()
// ... some work ...
elapsed := time.Since(start) // equivalent to time.Now().Sub(start)
This is perfect for logging how long operations take:
func processData(data []byte) Result {
defer func() {
log.Printf("Processing took %v", time.Since(start))
}()
start := time.Now()
// actual processing...
return result
}
The Until()
helper function
The Until()
function is the opposite of Since()
:
func Until(t Time) Duration
It calculates the duration until a future time t
. This is useful for determining how much time remains before a deadline:
deadline := time.Now().Add(5 * time.Minute)
// ... some work ...
remaining := time.Until(deadline) // time left until the deadline
if remaining < 0 {
log.Println("Deadline has passed!")
}
The beauty of these functions is that they all use the monotonic clock component when available, making your time measurements resilient to system clock changes. By sticking to these standard library functions, you get the benefits of monotonic time without having to manage different clock types explicitly.
One crucial detail to remember: the monotonic clock is only useful for measuring elapsed time within a single process. If you need to compare times across processes or machines, you'll need to rely on wall clock time and deal with its limitations.
Handling cases where the system clock changes
System clock changes are an inevitable reality in computing environments. Whether it's due to NTP synchronization, manual adjustments, daylight saving time transitions, or virtual machine operations, your application needs to handle these scenarios gracefully. Understanding how Go's time package behaves during clock changes can help you write more robust code.
Types of system clock changes
Before diving into handling strategies, let's understand the different types of clock changes:
- Forward jumps: The clock suddenly advances (e.g., from 1:59 AM to 3:00 AM during DST transitions)
- Backward jumps: The clock moves backward (e.g., from 2:59 AM to 2:00 AM when DST ends)
- Gradual adjustments: NTP typically makes small, gradual adjustments to align system time
Each of these can affect your application differently, especially if you're relying on wall clock time.
How Go handles clock changes
When a system clock change occurs, Go's behavior depends on whether you're using the monotonic component or just the wall clock time:
With monotonic component (using time.Now()
and duration methods like Sub()
, Since()
, Until()
):
- Duration calculations remain accurate regardless of clock changes
- Time comparisons (using
Before()
,After()
,Equal()
) use the monotonic component when available - Sleep and timer operations continue to work as expected
Without monotonic component (after serialization/deserialization or when using time constants):
- Duration calculations may be affected by clock changes
- Time comparisons reflect wall clock changes
- Sleep and timer operations might behave unexpectedly
Here's how various operations behave during a clock change:
// Before a backward clock change:
start := time.Now()
// ... system clock moves backward 1 hour ...
elapsed := time.Since(start) // Still accurate, reports actual elapsed time
// Using wall clock for a deadline:
deadline := time.Now().Add(1 * time.Hour)
// ... system clock moves backward 1 hour ...
remaining := time.Until(deadline) // Will report ~2 hours remaining!
Strategies for handling clock changes
When building systems that need to be resilient to clock changes, consider these approaches:
-
Rely on monotonic time for durations: Always use
time.Now()
and the standard duration methods for measuring elapsed time.
start := time.Now()
// Operation that needs to be timed accurately
elapsed := time.Since(start)
- Be cautious with deadlines based on wall time: If you're calculating deadlines far in the future, be aware that clock changes can affect them.
// Instead of this:
deadline := time.Now().Add(24 * time.Hour)
// Consider using an absolute time:
deadline := time.Date(2023, time.April, 16, 14, 30, 0, 0, time.Local)
-
Use context deadlines for timeouts: Go's
context
package integrates well with the time package for handling timeouts.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// This will respect the monotonic clock for the timeout duration
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
- Be explicit about time comparisons: When comparing times that might not have monotonic components, be aware of potential issues during clock changes.
// This might behave unexpectedly during clock changes if either time
// loses its monotonic component
if savedTime.Before(time.Now()) {
// Do something
}
The key to handling system clock changes is to understand when you're dealing with pure wall clock time versus time values that maintain their monotonic components. By leveraging Go's automatic integration of monotonic time, most of your code can be resilient to clock changes without special handling.
When and how to strip monotonic clock readings
There are specific scenarios where you might need to explicitly work with just the wall clock component of a time.Time
value, stripping away the monotonic reading. Go provides mechanisms to do this, but it's important to understand when this is necessary and how to do it properly.
When to strip monotonic readings
You typically need to strip monotonic clock readings in the following situations:
Serialization/Persistence: When storing time values that will be used across process boundaries or after process restarts.
Cross-system time comparison: When comparing times generated on different machines.
Deterministic testing: When you need consistent time representations regardless of when the test runs.
Time zone or calendar operations: When performing operations that only make sense with wall clock time.
Logging or displaying time: When the monotonic component isn't relevant to humans reading the output.
How to strip monotonic readings
Go provides a straightforward method to remove the monotonic clock component from a time.Time
value:
func (t Time) Round(d Duration) Time
While Round
is primarily designed to round a time to the nearest multiple of a duration, it has a side effect of stripping the monotonic clock reading. Any non-zero duration will work:
// Strip monotonic component
wallTime := time.Now().Round(0)
Another option is to use the time.Unix()
function to convert to Unix time and back:
now := time.Now()
// Strip monotonic component by converting to Unix time and back
wallTime := time.Unix(now.Unix(), int64(now.Nanosecond()))
Starting with Go 1.9, there's also an explicit method for this purpose:
// Returns true if t has a monotonic clock reading
hasMonotonic := t.GoString() != t.String()
// Create a new time without monotonic clock reading
wallTime := t.Truncate(0)
Practical examples
Let's look at some practical examples where stripping monotonic readings is useful:
- Storing time in a database:
func storeEventTime(db *sql.DB, eventID string, eventTime time.Time) error {
// Strip monotonic component before storing
wallTime := eventTime.Round(0)
_, err := db.Exec("INSERT INTO events (id, timestamp) VALUES (?, ?)",
eventID, wallTime)
return err
}
- JSON serialization:
type Event struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
}
func serializeEvent(event Event) ([]byte, error) {
// Create a copy with wall time only for serialization
eventCopy := Event{
ID: event.ID,
Timestamp: event.Timestamp.Round(0),
}
return json.Marshal(eventCopy)
}
- Deterministic testing:
func TestTimeFormatting(t *testing.T) {
// Create a fixed wall time for testing
fixedTime := time.Date(2023, 4, 15, 12, 30, 0, 0, time.UTC)
// This will be consistent across test runs
formatted := formatTimeForDisplay(fixedTime)
if formatted != "12:30 PM" {
t.Errorf("Expected '12:30 PM', got '%s'", formatted)
}
}
Best practices
When working with time in Go, follow these best practices regarding monotonic clock readings:
Default to keeping monotonic readings for all time measurements within a process.
Explicitly strip monotonic readings only when necessary (serialization, cross-process communication).
Document when you're stripping monotonic readings, as it affects how time values can be used.
Be consistent in your approach within a given module or library.
Test time-sensitive code with both monotonic and wall clock time to ensure proper behavior.
By understanding when and how to strip monotonic clock readings, you can ensure that your time-related code handles both precise duration measurements and proper wall clock operations correctly.
Top comments (0)