skip to content

In Ruby, why should you time an operation with Process.clock_gettime(Process::CLOCK_MONOTONIC) rather than subtracting two Time.now values?

level: middleimportance: should knowfreq 48%

answer

  1. wall clock can be reset
  2. never goes backwards
  3. origin is arbitrary
  4. default unit :float_second
  5. CPU time is a different clock

basics

~10 s

Time.now reads the wall clock, which NTP or an operator can step forwards or backwards mid-measurement. Process.clock_gettime(Process::CLOCK_MONOTONIC) never goes backwards, so the difference of two readings is a reliable elapsed time in seconds.

solid answer

~40 s

`Time.now` is **wall-clock** time: it answers "what time is it", and the system may step it when NTP corrects drift, an admin changes it, or a VM resumes. Subtracting two `Time.now` values can then give a negative or inflated duration. `Process.clock_gettime(Process::CLOCK_MONOTONIC)` reads a clock that only moves forward, with an arbitrary origin such as boot time, so only differences between readings mean anything. It returns a `Float` of seconds by default; pass a unit such as `:millisecond` or `:nanosecond` for an `Integer`. Use it for timeouts, retry back-off, latency logging and rate limits. Keep `Time.now` for timestamps a person or another system reads, and use `:CLOCK_PROCESS_CPUTIME_ID` when you want CPU time rather than elapsed time.

code

ruby · 9 lines
ruby
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
sleep 0.25
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
# => about 0.25 (Float seconds)

cpu0 = Process.clock_gettime(:CLOCK_PROCESS_CPUTIME_ID, :millisecond)
sleep 0.25
Process.clock_gettime(:CLOCK_PROCESS_CPUTIME_ID, :millisecond) - cpu0
# => close to 0: sleeping uses no CPU

go deeper

for a junior

Recall that Time.now can jump and that Process.clock_gettime with CLOCK_MONOTONIC is the tool for measuring how long something took.

for a middle

Explain what makes the wall clock jump, why the monotonic origin is arbitrary, the default Float seconds unit, and the integer unit options.

for a senior

Show where wall-clock durations break production: negative timeouts, skewed latency metrics, retry deadlines. Use CPU time against elapsed time to separate waiting from computing.

for a principal

Make clock choice a codebase rule: wrap monotonic timing in one helper for deadlines and metrics so no team reinvents duration code with Time.now.

## Two different questions A program asks two different time questions, and they need two different clocks: 1. **"What time is it?"** A timestamp for a log line, a record or a user. This is **wall-clock** or *real* time, the clock `Time.now` reads. 2. **"How long did this take?"** An elapsed duration for a timeout, a back-off, a latency metric. This needs a clock that measures intervals faithfully: a **monotonic** clock. ## Why wall-clock subtraction goes wrong The wall clock is allowed to **jump**. Operating systems change it when: - a time-synchronisation daemon steps the clock to correct drift; - an administrator or a container host sets the time; - a virtual machine or laptop resumes from suspend and catches up. If a jump happens between two `Time.now` readings, `finish - start` is off by the size of the jump. It can even be **negative**, which turns a timeout check into an infinite wait or makes a latency histogram report nonsense. The bug is rare, which makes it hard to reproduce: it appears only when a correction lands inside the measured window. ## Process.clock_gettime `Process.clock_gettime(clock_id, unit = :float_second)` wraps the POSIX `clock_gettime()` call. | Clock | Measures | Use it for | |---|---|---| | `Process::CLOCK_MONOTONIC` | elapsed time, never backwards, arbitrary origin | durations, timeouts, back-off | | `Process::CLOCK_REALTIME` | wall-clock seconds since the epoch | rarely; `Time.now` is recommended instead | | `Process::CLOCK_PROCESS_CPUTIME_ID` | CPU time consumed by this process | telling CPU work from waiting | The clock can be given as the constant or as a symbol such as `:CLOCK_MONOTONIC`. Which clocks exist depends on the operating system; an unsupported one raises `Errno::EINVAL`. `CLOCK_MONOTONIC` is available on Linux, macOS, the BSDs and Windows, with emulations where needed. The **origin** of the monotonic clock is system-dependent (often boot time), so a single reading is meaningless on its own. Never store it, compare it across processes or machines, or turn it into a date. ## Units The optional second argument picks the unit: - `:float_second` (default), `:float_millisecond`, `:float_microsecond` return a `Float`; - `:second`, `:millisecond`, `:microsecond`, `:nanosecond` return an `Integer`. Integers avoid floating-point rounding when you sum many small intervals or need exact nanoseconds. ## A reusable pattern ```ruby def elapsed_ms start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) yield Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - start end ``` The same idea powers a deadline: compute `deadline = now + budget` once on the monotonic clock, then compare each new reading against it inside a retry loop. ## Deadlines in retry loops A retry loop with an overall budget is where the monotonic clock matters most: 1. Read the monotonic clock once and compute `deadline = start + budget_seconds`. 2. Before each attempt, compute `remaining = deadline - now` from a fresh monotonic reading. 3. Stop when `remaining` reaches zero, and pass `remaining` to any call that accepts a timeout, so the whole loop respects one budget. With `Time.now` instead, a clock step backwards extends the budget by the size of the step, and a step forwards ends it early. ## Mistakes seen in review - Passing a monotonic reading to `Time.at`: the result is a meaningless date, typically in early 1970, because the reading counts from an arbitrary origin. - Mixing clocks: subtracting a `Time.now.to_f` start value from a monotonic reading. - Summing many `Float` intervals where exact totals matter, instead of an integer unit such as `:nanosecond`. - Measuring CPU-bound work by elapsed time on a busy machine, where other processes inflate the result. ## Wall time versus CPU time Elapsed monotonic time includes waiting on I/O, locks and sleeping. If a request takes 800 ms of monotonic time but only 20 ms of `CLOCK_PROCESS_CPUTIME_ID`, it spent its time waiting, not computing. Taking both readings around the same block is a cheap first diagnostic before reaching for a profiler. ## Summary - `Time.now` for timestamps; the monotonic clock for durations. - Differences only; the origin is arbitrary. - Pick an integer unit when precision matters. - CPU-time clocks answer a different question from elapsed time.

  • In Ruby, can you log the value of Process.clock_gettime(Process::CLOCK_MONOTONIC) as a timestamp?
    No. Its origin is system-dependent, often boot time, so a single reading has no calendar meaning and differs between machines and reboots. Log a `Time.now` timestamp, in ISO 8601, for when something happened, and a monotonic difference for how long it took.
  • In Ruby, how do you tell whether a slow block was computing or waiting?
    Read both `Process::CLOCK_MONOTONIC` and `Process::CLOCK_PROCESS_CPUTIME_ID` before and after it. A large monotonic difference with a small CPU-time difference means the block was waiting on I/O, locks or sleep; similar values mean it was burning CPU.

The wall clock is the office clock that anyone may reset; the monotonic clock is a stopwatch in your pocket. You would never time a race by glancing at a clock someone might adjust halfway through.

saying these in an interview costs you the question

  • Time.now - start is always accurate, because the system clock only moves forward.
  • A monotonic reading is seconds since the Unix epoch.
  • Process.clock_gettime returns an Integer of nanoseconds by default.
  • CLOCK_PROCESS_CPUTIME_ID measures wall-clock time for the process.
  • Monotonic readings can be compared between two servers.