C++ chrono::time_point: Clocks, Epochs, Timeouts, and Elapsed Time
Key takeaways
Measuring elapsed time with system_clock can go wrong when the wall clock is adjusted, which is why steady_clock exists. The post covers clock types and their compatibility, precision loss when casting, C++20 time zone support, and patterns for timeout checks, periodic tasks, log timestamps and serializing time points.
What is time_point?
std::chrono::time_point represents a point in time above a specific clock. It is used with duration, the resolution can be changed with time_point_cast, and when measuring elapsed time in a stopwatch or benchmark, the reference point is set with now().
#include <chrono>
auto now = std::chrono::system_clock::now();
auto epoch = std::chrono::system_clock::time_point{};
system_clock::time_point{} — default-constructed, with no arguments — isn’t garbage or an error state; it’s specifically the epoch, because a default-constructed duration (which is what time_point stores internally) is zero, and zero duration since the epoch is the epoch. This is worth knowing before reaching for a default-constructed time_point as a sentinel “no value yet” marker, since it actually corresponds to a very real, very old timestamp (1970-01-01 for system_clock) rather than an obviously-invalid one.
now(), epochs, and time_since_epoch
using namespace std::chrono;
// Current time
auto now = system_clock::now();
// Time since epoch
auto duration = now.time_since_epoch();
auto ms = duration_cast<milliseconds>(duration);
std::cout << ms.count() << "ms" << std::endl;
How it works: time_point stores a duration from an epoch. The epoch of system_clock is usually 1970-01-01 00:00:00 UTC.
// time_point structure (conceptual)
template<typename Clock, typename Duration>
class time_point {
Duration d_; // duration since the epoch
public:
Duration time_since_epoch() const { return d_; }
};
This conceptual sketch is the key to understanding almost every time_point quirk in this guide: a time_point is fundamentally just a duration plus a tag saying which Clock it’s measured from. There’s no wall-clock date or calendar arithmetic baked in anywhere — “what time is it” only becomes a human-readable date once you convert through to_time_t or a C++20 calendar type, which is exactly why comparing or subtracting time_points from different clocks doesn’t compile: their durations are measured from different, unrelated reference points, so combining them would be comparing apples to oranges with no way for the compiler to know how far apart those oranges started.
Elapsed time, timestamps, deadlines, and comparisons
Measuring elapsed time with steady_clock
auto start = std::chrono::steady_clock::now();
// Work being timed
std::this_thread::sleep_for(std::chrono::seconds(1));
auto end = std::chrono::steady_clock::now();
auto elapsed = end - start;
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
std::cout << "Elapsed: " << ms.count() << "ms" << std::endl;
steady_clock, not system_clock, is the deliberate choice here — it’s the standard’s dedicated clock for elapsed-time measurement precisely because it’s guaranteed monotonic (it never runs backward, even if the OS clock is adjusted by NTP or a user), unlike system_clock.
A Unix timestamp from system_clock
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::system_clock::to_time_t(now);
std::cout << "Timestamp: " << timestamp << std::endl;
std::cout << "Time: " << std::ctime(×tamp);
A point in the future
using namespace std::chrono;
auto now = system_clock::now();
auto future = now + hours(24); // 24 hours from now
auto futureTime = system_clock::to_time_t(future);
std::cout << "24 hours from now: " << std::ctime(&futureTime);
Comparing two time points
auto t1 = std::chrono::system_clock::now();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto t2 = std::chrono::system_clock::now();
if (t2 > t1) {
std::cout << "t2 is later" << std::endl;
}
auto diff = t2 - t1;
t2 > t1 here will be true in virtually every real run, but it’s not a hard guarantee — if the system clock is adjusted backward between the two now() calls (an NTP correction, a user changing the clock), t2 < t1 becomes possible even though 100ms of wall-clock time genuinely passed. Comparisons and subtractions on system_clock::time_point should be treated as “usually forward” rather than “always forward” for exactly this reason — steady_clock is the type that actually guarantees the ordering this code is implicitly assuming.
system_clock, steady_clock, and high_resolution_clock
// system_clock: wall-clock time
auto sys = std::chrono::system_clock::now();
// steady_clock: monotonically increasing
auto steady = std::chrono::steady_clock::now();
// high_resolution_clock: highest available resolution
auto high = std::chrono::high_resolution_clock::now();
Special features of each clock:
| clock | Characteristics | epoch | Usage Scenarios |
|---|---|---|---|
system_clock | Actual time, affected by system time changes | 1970-01-01 UTC | timestamp, file time |
steady_clock | Monotonically increasing, independent of system time changes | Implementation dependent | Elapsed time, timeout |
high_resolution_clock | highest resolution | Implementation dependent | Short section measurement |
Practice Recommendations:
- Requires actual time:
system_clock(log, file modification time) - Elapsed time measurement:
steady_clock(timeout, benchmark)
// ✅ Log timestamp: system_clock
auto now = system_clock::now();
auto time_t = system_clock::to_time_t(now);
std::cout << "Log at: " << std::ctime(&time_t);
// ✅ Elapsed time measurement: steady_clock
auto start = steady_clock::now();
// ... work ...
auto elapsed = steady_clock::now() - start;
high_resolution_clock is conspicuously absent from the recommendations above despite sounding like the obvious choice for “measure something precisely” — the standard doesn’t require it to be its own distinct clock at all. On most standard library implementations it’s simply a type alias for either system_clock or steady_clock (which one is implementation-defined and has actually changed between library versions on some platforms), so code that picks high_resolution_clock for benchmarking can silently inherit system_clock’s non-monotonic behavior on some platforms and steady_clock’s guarantees on others. Reaching for steady_clock explicitly avoids depending on which alias your particular standard library happens to choose.
Clock adjustments, mixed clocks, truncation, and time zones
The system clock jumping backward
// ❌ system_clock (affected by system time changes)
auto start = std::chrono::system_clock::now();
// ...system time gets changed here...
auto end = std::chrono::system_clock::now();
// duration could come out negative
// ✅ steady_clock
auto start = std::chrono::steady_clock::now();
auto end = std::chrono::steady_clock::now();
// always non-negative
Mixing time points from different clocks
auto sys = std::chrono::system_clock::now();
auto steady = std::chrono::steady_clock::now();
// ❌ Comparing different clocks
// auto diff = sys - steady; // compile error
// Use the same clock on both sides instead
This is a compile-time error rather than a runtime surprise, which is one of the nicer safety properties of the time_point/Clock design — time_point<system_clock, D1> and time_point<steady_clock, D2> are simply different, unrelated types as far as the compiler is concerned, so there’s no operator- overload connecting them at all. You find out about the mistake immediately at compile time, not after shipping code that occasionally produces a nonsensical duration.
duration_cast truncating instead of rounding
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
// Anything finer than millisecond resolution is lost here
duration_cast between a finer and a coarser duration truncates rather than rounds — casting system_clock::now() (often nanosecond-resolution internally) down to milliseconds discards the sub-millisecond remainder entirely rather than rounding to the nearest millisecond. For most logging and timestamp purposes that’s fine, but it’s worth knowing explicitly if you’re ever comparing a truncated timestamp against a boundary condition, since the truncation always rounds toward the epoch, never away from it.
Time zones before and after C++20
// system_clock is UTC internally
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
// Converting to local time
std::cout << std::ctime(&tt); // local time
C++20 Time Zone Support:
#include <chrono>
#include <iostream>
using namespace std::chrono;
// UTC time
auto now = system_clock::now();
// Local time zone
auto local = zoned_time{current_zone(), now};
std::cout << "Local: " << local << '\n';
// A specific time zone
auto ny = zoned_time{"America/New_York", now};
auto tokyo = zoned_time{"Asia/Tokyo", now};
std::cout << "New York: " << ny << '\n';
std::cout << "Tokyo: " << tokyo << '\n';
Before C++20, converting to a named time zone like "America/New_York" meant reaching for a third-party library (Howard Hinnant’s date library, which directly inspired this standardized API) — there was no portable standard-library way to do it. std::ctime and std::localtime only ever give you the running machine’s local time zone, with no way to ask for a different one; zoned_time is what actually closes that gap.
Arithmetic between time points and durations
using namespace std::chrono;
auto now = system_clock::now();
// Add
auto future = now + hours(1);
// Subtract
auto past = now - minutes(30);
// Difference
auto diff = future - past;
Operation rules:
| operations | Result Type | Example |
|---|---|---|
time_point + duration | time_point | now + 1h |
time_point - duration | time_point | now - 30min |
time_point - time_point | duration | end - start |
time_point == time_point | bool | t1 == t2 |
time_point < time_point | bool | t1 < t2 |
Practical example:
// Timeout check
auto deadline = steady_clock::now() + 5s;
while (steady_clock::now() < deadline) {
if (try_operation()) break;
std::this_thread::sleep_for(100ms);
}
// Checking file age
auto file_time = fs::last_write_time("file.txt");
auto now = fs::file_time_type::clock::now();
auto age = now - file_time;
if (age > 24h) {
std::cout << "File is more than 24 hours old\n";
}
Timeouts, periodic tasks, and log timestamps
A deadline-based timeout check
class TimeoutChecker {
steady_clock::time_point deadline_;
public:
TimeoutChecker(milliseconds timeout)
: deadline_(steady_clock::now() + timeout) {}
bool expired() const {
return steady_clock::now() >= deadline_;
}
milliseconds remaining() const {
auto now = steady_clock::now();
if (now >= deadline_) return milliseconds::zero();
return duration_cast<milliseconds>(deadline_ - now);
}
};
// Usage
TimeoutChecker checker(5s);
while (!checker.expired()) {
std::cout << "Time remaining: " << checker.remaining().count() << "ms\n";
std::this_thread::sleep_for(1s);
}
Storing the absolute deadline_ computed once at construction, rather than storing the timeout duration and re-adding it to now() on every check, is the detail that makes this correct — a timeout that re-derives its deadline from “now + original timeout” on every check would keep pushing the deadline forward every time it’s checked, never actually expiring.
Scheduling periodic tasks
class PeriodicTask {
steady_clock::time_point next_run_;
milliseconds interval_;
public:
PeriodicTask(milliseconds interval)
: next_run_(steady_clock::now()), interval_(interval) {}
bool should_run() {
auto now = steady_clock::now();
if (now >= next_run_) {
next_run_ = now + interval_;
return true;
}
return false;
}
};
// Usage
PeriodicTask task(1s); // once per second
while (true) {
if (task.should_run()) {
std::cout << "Running task\n";
}
std::this_thread::sleep_for(100ms);
}
Computing next_run_ = now + interval_ (based on the actual firing time) rather than next_run_ += interval_ (based on the previous scheduled time) is a deliberate choice with a real consequence: if the surrounding loop occasionally runs late (a slow iteration, a scheduling hiccup), this version doesn’t try to “catch up” with a burst of rapid-fire calls — each new deadline is set relative to when the task actually last ran, not to where it was originally supposed to be. That’s usually what you want for a polling loop; a scheduler that needs to guarantee exactly N runs per second regardless of delays would need the accumulating version instead, accepting the catch-up bursts as the cost of that guarantee.
Formatting log timestamps
std::string format_timestamp(system_clock::time_point tp) {
auto time_t = system_clock::to_time_t(tp);
auto ms = time_point_cast<milliseconds>(tp);
auto ms_part = ms.time_since_epoch().count() % 1000;
char buf[64];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&time_t));
return std::string(buf) + "." + std::to_string(ms_part);
}
// Usage
void log(const std::string& msg) {
auto now = system_clock::now();
std::cout << format_timestamp(now) << " " << msg << '\n';
}
Note that std::localtime (used here via std::strftime) is not thread-safe on most platforms — it typically returns a pointer into a shared static buffer, so calling format_timestamp concurrently from multiple threads is a data race. localtime_r (POSIX) or localtime_s (Windows) are the thread-safe equivalents, and are worth swapping in if this logging function is ever called from more than one thread, which is a common upgrade path once a single-threaded logging utility gets reused in a multi-threaded service.
time_point structure (template and semantics)
std::chrono::time_point<Clock, Duration> stores the point in time based on the epoch of the corresponding Clock, as a Duration.
Clock: Provides nested types such asnow(),time_point, andduration. Thetime_pointof differentClocks cannot be directly subtracted or compared.- Duration: Usually
Clock::durationor a suitablestd::chrono::duration<Rep, Period>. Even for the same clock,time_point<steady_clock, milliseconds>andtime_point<steady_clock, nanoseconds>are different types, so usetime_point_castwhen converting between them.
using namespace std::chrono;
steady_clock::time_point t1 = steady_clock::now();
auto t2 = time_point_cast<milliseconds>(t1); // only the resolution changes, same clock
The default value time_point{} often points to the epoch, so it is safer to use std::optional or a separate flag when expressing “no value” in an API.
Relationship with duration (advanced)
time_pointis the “point in time”, anddurationis the “interval”.- On the same clock:
time_point - time_point→duration,time_point ± duration→time_point. - epoch:
tp.time_since_epoch()is thedurationfrom the epoch ofClock. Forsystem_clock, it is usually ticks based on 1970-01-01 UTC.
To deal with calendar dates (year/month/day), a conversion flow to a calendar type such as C++20’s std::chrono::year_month_day is required, and time_point alone does not express all leap second and time zone rules. I recommend looking at it along with the Calendar & Timezone topic.
Rules for sleep_until, mixing clocks, and rounding
- Do not mix clocks:
system_clock::now() - steady_clock::now()will not compile and is meaningless. sleep_untiland clock: Use asteady_clock::time_pointto set the wake-up time so it won’t be affected by system time changes. If you only usesystem_clock::now() + 5s, it may behave differently than intended when the user changes the system time.- Overflow: Adding extremely large
durations can exceed implementation/domain limits, so it is safe to have a “maximum duration” rule.
Timestamps and expiration times in real systems
Time stamp (log/API): Usually, take system_clock::now(), convert time_since_epoch() to milliseconds, and put it in JSON/Protobuf. Unifying to UTC and applying the local time zone only when displaying will reduce confusion.
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count();
// wire: int64_t ms since Unix epoch (UTC)
Expiration time (token/cache): If the “real time” is important, store expires_at with system_clock::time_point. If you want to use only the duration regardless of device power saving or time changes, such as “5 minutes after registration”, set the standard as steady_clock, or if the domain allows, save everything based on the UTC epoch instead.
Serialization (Wire format)
For compatibility with other processes and languages, we recommend the following:
- Integer since Unix epoch: Document the units as one of nano/micro/millisecond. Example:
int64_t milliseconds_since_epoch. - ISO 8601 string: Suitable for human reading or logging. Convert
system_clock→time_t/tm, or use a C++20 formatter. - Specify clock type: The
time_pointofsteady_clockis meaningless on other machines even if serialized. Do not use it for restarts or an “expires” value shared with other machines. - Deserialize: Beware of overflow and unit confusion (µs vs ms) when restoring integer →
duration→system_clock::time_point.
std::chrono::system_clock::time_point tp{
std::chrono::milliseconds{millis_from_wire}
};
In binary protocols, both endianness and signed 64-bit range should be considered — a signed 64-bit millisecond count comfortably covers dates for hundreds of millions of years in either direction, so overflow is a non-issue at that width, but a 32-bit count (still common in older wire formats) overflows well within a human lifetime.
FAQ
Q1: What is time_point?
A: A type that represents the elapsed time from the epoch of a specific clock. Represents a specific point in time.
Q2: Which clock should I use?
A:
- system_clock: When actual time is required (log, file time)
- steady_clock: When measuring elapsed time (timeout, benchmark)
Q3: How to convert to a timestamp?
A: Use system_clock::to_time_t().
auto now = system_clock::now();
auto time_t = system_clock::to_time_t(now);
std::cout << std::ctime(&time_t);
Q4: How do you calculate time?
A: You can use +, -, and comparison operators. time_point + duration = time_point, time_point - time_point = duration.
Q5: What clock is used to measure performance?
A: steady_clock is recommended. Monotonic increase is guaranteed, unaffected by system time changes.
Q6: What is C++20 time zone support?
A: Time zone conversion is possible using zoned_time and time_zone.
auto now = system_clock::now();
auto ny = zoned_time{"America/New_York", now};
Q7: What is the difference between time_point and duration?
A:
- time_point: A specific point in time (e.g. “2026-03-12 14:30:00”)
- duration: time interval (e.g. “5 seconds”)
Q8: What is epoch?
A: The reference point of the clock. The epoch of system_clock is usually 1970-01-01 00:00:00 UTC.
Q9: What are time_point learning resources?
A:
- “C++ Primer” by Lippman, Lajoie, Moo
- “Effective Modern C++” by Scott Meyers
- cppreference.com - std::chrono::time_point
One line summary: time_point represents a point in time on a specific clock and can be used with duration to perform time operations.
Related Articles
- C++ std::chrono::duration
- C++ std::chrono: duration, time_point, Clocks, duration_cast and C++20 Calendar
- steady_clock vs system_clock vs high_resolution_clock
- C++ Benchmarking
- C++20 Calendar and Time Zones