In the documentation of Go’s time
package you will find the term “monotonic clock” roughly 30 times. However, you likely won’t ever work with it directly. So, what is it for and why should you care?
In the previous article, we looked at how time.Time
values represent “time instants” on timelines using a time.Location
reference.
When created using time.Now()
a time.Time
can also contain a different time representation: The monotonic clock reading.
It’s represented on a timeline that is entirely seperate from to the ones provided by time.Location
.
For example, a time.Time
value in the time.UTC
location with a monotonic clock reading would look like this when visualized:
In this article we will take a look at:
- What it means for a clock to be monotonic.
- Why this is a desired property when “measuring time”.
- How this all works in the
time
package. - And finally, what it means for your programs.
Let’s check
To begin, let’s take a look at a monotonic clock reading for ourselves.
Run the example below and take a look at the output.
package main
import (
"fmt"
"time"
)
func main() {
tUTC := time.Date(2023, 12, 19, 13, 37, 0, 0, time.UTC)
tNow := time.Now()
fmt.Println(tUTC.String())
fmt.Println(tNow.String())
}
As you can see, the time created via time.Now()
contains an additional value at the end of its String()
output. Something like:
... m=+0.000000001
This is the monotonic clock reading.
It’s included in the output of the String()
method for debugging purposes, but it’s not exported by the time.Time
type.
To explain why it’s not exported, we need to zoom out a bit and look at monotonic clocks and “wall clocks”.
What is a monotonic clock?
A clock is monotonic when it gives a guarantee about its readings: Every new reading is always greater than or equal to (>=
) the previous reading.
In other words, it can’t be “turned back”.
For example, suppose we have a clock that always returns integer values.
The following readings are monotonic:
1 2 2 3 4 5 6 8 10
Every reading is either:
- Equal to the previous reading.
- Greater than the previous reading.
These readings are not monotonic:
1 2 2 1 2 3 3 4 6
Inbetween the third (2
) and fourth (1
) reading the clock was “set back” one unit, resulting in the fourth reading being lower than the one before.
Conceptually you might expect all clocks to be monotonic, but that’s not the case.
What is a wall clock?
When you look at a clock on the wall and say “it’s 13:37” that might seem like an easy thing to do.
However, if you dig a bit deeper it quickly turns rather complex: These wall clocks are generally coordinated using UTC and bound to a geographic location.
The coordination depends on human factors and is a social issue. I won’t dig into the details (check out my guide for that), but the rules change all the time in ways that can’t be computed or predicted:
- Daylight saving time.
- Time zone changes.
- Leap seconds.
As a result wall clocks are turned back all the time.
They are not monotonic.
Telling time vs measuring time
The difference between wall clocks and monotonic clocks is sometimes described as:
- Wall clocks are for “telling time”: assigning a useful meaning to a clock reading.
- Monotonic clocks are for “measuring time”: showing the passage of time between clock readings.
A wall clock can be used to tell time in a way that is coordinated with the rest of the world. For example, you can see it’s 13:37
in Iceland, and determine that corresponds to 12:37
in Cape Verde.
Wall clocks are inconvenient for measuring time. If you attempt to measure the interval between two clock readings, you need to compensate for any changes made to the clock inside that interval.
In contrast, to get the interval of two monotonic clock readings (from the same clock) you can subtract one from the other. There is no risk of the clock being set back.
However, monotonic clocks are not convenient for coordinating time with the rest of the world. The world sets back their clocks all the time.
System clock and monotonic clock
This is why most computer systems offer two clocks.
The system clock is a wall clock, it’s coordinated with the rest of the world and can be used to tell time.
The monotonic clock is a monotonic clock that is reset when the system restarts, it’s only guarantee is that it ticks forward (at a consistent pace). Clock readings are not coordinated, but can be used for measuring time while a program is running.
If you need to measure time across system restarts, you will need to fall back to using the system clock.
time.Time
type. It doesn't have any meaning outside of the "current system boot" context.
Every system has a system clock, and most systems have a monotonic clock.
Reading clocks in Go: time.Now()
In Go the time.Time
is used for both telling time and measuring time.
To achieve this, the time.Time
type can store two elements: a wall time element and a monotonic element.
The wall time element is always present and works using the time.Location
mechanism discussed earlier.
time
package only used the wall time, this lead to the Cloudflare leap second bug at the very end of 2016.
After that the monotonic clock was introduced in the Go time package.
Calling time.Now()
will create a new time.Time
value and set:
- The wall time element to the current system clock reading.
- The monotonic element to the current monotonic clock reading (if the system has one).
Let’s take a look at a very unsafe example that shows these elements in use.
package main
import (
"fmt"
"time"
"unsafe"
)
func main() {
before := time.Now()
// subtract 60 seconds from the monotonic element of
// before. It now contains mismatched wall time element
// and monotonic element.
unsafeAddMonotonic(&before, -60*time.Second)
// print it.
fmt.Println(before.Format(time.RFC3339))
// sleep for 3 seconds.
time.Sleep(3.0 * time.Second)
after := time.Now()
// print after
fmt.Println(after.Format(time.RFC3339))
// print the duration between before and after.
fmt.Printf("took %.1fs\n", after.Sub(before).Seconds())
}
// unsafeAddMonotonic that adds the provided value to the
// unexported ext field of the t.
// ONLY FOR THIS EXAMPLE. Never do this in your code.
func unsafeAddMonotonic(t *time.Time, add time.Duration) {
var sample int64
ptrToT := unsafe.Pointer(t)
ptrToT = unsafe.Pointer(uintptr(ptrToT) + unsafe.Sizeof(sample))
ptrToExt := (*int64)(ptrToT)
*ptrToExt += int64(add)
}
In this example we create a mismatch between the two elements by (unsafely!) subtracting 60s
from the monotonic element of the before
value.
This 60s
difference isn’t reflected in the times we see when displaying before
and after
as RFC3339 timestamps:
2009-11-10T23:00:00Z
2009-11-10T23:00:03Z
This is because the Format
function only considers the wall time elements of the before
and after
values. It’s a “time telling” operation.
However, it is visible in the duration we compute using after.Sub(before)
:
took 63.0s
This shows that the Sub
method uses the monotonic elements in the time.Time
values. In other words: it’s a “time measuring” operation.
Monotonic-first functions
The only way to create a time.Time
value with a monotonic element is:
- Using the
time.Now
function (on a system with a monotonic clock). - Deriving a new
time.Time
value by callingt.Add(u)
on at
value with a monotonic element.
The monotonic element is used by a limited set of methods:
t.After(u)
t.Before(u)
t.Equal(u)
t.Compare(u)
t.Sub(u)
These methods all use the monotonic element when it’s present in both t
and u
. It falls back to using the wall time element when it’s missing in t
, u
or both of them.
The t.Add(d)
method adds the duration d
to both the wall time and the monotonic element (if it’s present in t
).
Wall time functions
All other functions only use or construct time.Time
values with a wall time element. Values returned by time.Date
, time.Parse
or time.Unix*
will never contain a monotonic element for example.
Noteably, some methods actively strip out the monotonic element:
t.AddDate(y, m, d)
t.Round(d)
t.Truncate(d)
Serializing functions
Since the monotonic element has no value outside of the “current system boot” context, it’s also never included when serializing a time.Time
value.
Calling t.Format(layout)
, t.Marshal*
or t.GobEncode()
will not include the monotonic element.
This means that serializing and unserializing a time.Time
value can be lossy. You will lose the monotonic element.
Practical tips
Practically this means that there are two kinds of time.Time
values:
- Those with only a wall time element.
- Those with both a wall time and monotonic element.
As we saw, the behavior of some methods is different depending on the kind of value they’re working with.
Test data
Usually you want test data to be consistent between test runs. To create consistent time.Time
values it’s common to
use time.Date
or time.Parse
. This will only ever create values with a wall time element.
If in production these values are created using time.Now()
you will end up with values containing both a wall time and a monotonic element.
This could result in different behavior between tests and production.
Depending on what you’re doing, this might (not) be an issue, but it’s good to be aware of it.
Remove monotonic element
The recommended way to remove the monotonic element of a time.Time
value is using a t.Round(0)
method call.
See this code snippet for an example.
Measure execution time
To measure execution time of an operation you can calculate the difference between two time.Time
values created via time.Now()
.
See the example below.
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
// emulate an operation by sleeping for 3 seconds.
time.Sleep(time.Second * 3)
finish := time.Now()
// finish - start = took
took := finish.Sub(start)
fmt.Printf("took %dms\n", took.Milliseconds())
}
Time not moving forward
The monotonic clock is guaranteed to not “move backwards”, but it can return the same value multiple times.
In other words: Two calls to time.Now()
might return the same time.Time
value.
For example:
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
t2 := time.Now()
switch t1.Compare(t2) {
case -1:
// should never happen
fmt.Println("t1 is after t2")
case 0:
// can happen
fmt.Println("t1 is equal to t2")
case 1:
// can happen
fmt.Println("t1 is before t2")
}
}
Assuming that only case 1
can happen will result in bugs.
Don’t assume UTC is monotonic
This article resulted in a question on Reddit that is worth repeating here:
Is UTC not monotonic? The examples given in the article are all geography based, which has its application but is often not relevant.
The UTC standard itself is monotonic. Time always moves forward.
However, applications don’t read UTC time directly.
They read UTC time from the system clock, which can potentially:
- Get out of sync and be adjusted by a network time protocol (NTP) service.
- Be adjusted by the OS.
- Be adjusted by the user.
In addition, systems handle UTC leap seconds in different ways. On some systems the same second gets repeated twice, making it look like the system clock was moved back a second.
Conclusion
In this article we took a look at the monotonic clock and its role in computing and more specifically it’s use in Go.
Key takeaways:
- A monotonic clock is a clock that can’t be “moved back”.
- Wall clocks are not monotonic clocks.
- Wall clocks are for telling time.
- Monotonic clocks are for measuring time.
- The
time.Time
type can contain both a wall element and a monotonic element. - Only
time.Now()
andt.Add(d)
can createtime.Time
values with a monotonic element. - “Monotonic first” functions use the monotonic element when possible.
- There are other functions that ignore or actively remove the monotonic element.
I hope this article helps you write robust, time-aware applications. Keep these concepts in mind when you build your next project.
As always, if you have any questions or comments, feel free to reach out.
Happy coding!
Get my free newsletter every second week
Used by 500+ developers to boost their Go skills.
"I'll share tips, interesting links and new content. You'll also get a brief guide to time for developers for free."