C++ std::chrono::duration: Units, Literals, and Safe Conversions

Key takeaways

std::chrono::duration puts the time unit into the type. Covers construction, chrono literals (C++14), which conversions are implicit and which need duration_cast/floor/ceil/round, and the C++20 calendar literals 2026y and 29d.

What is duration?

std::chrono::duration is a type that represents a time interval. It is one of the two fundamental building blocks of the chrono library, alongside time_point: a time_point is “a moment on some clock”, a duration is “how far apart two moments are”.

A duration has two template parameters: a representation type (Rep, the type of the tick count) and a period (Period, a std::ratio saying how many seconds one tick is).

#include <chrono>

// std::chrono::milliseconds is duration<some signed integer, std::ratio<1, 1000>>
std::chrono::seconds      sec(10);
std::chrono::milliseconds ms(1000);
std::chrono::minutes      min(1);

// A custom one: tick = 1/60 s, stored as double
using frames = std::chrono::duration<double, std::ratio<1, 60>>;

The whole point is that the unit lives in the type. A function declared as void set_timeout(std::chrono::milliseconds) cannot be called with a raw 5 that someone meant as seconds; the call site has to say 5s or 5000ms, and either one arrives as the correct number of milliseconds.

Basic Usage

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;

    seconds s(10);
    milliseconds ms(10000);

    std::cout << s.count() << '\n';   // 10 (the raw tick count)
    std::cout << (s == ms) << '\n';   // 1: comparisons work across units
}

count() gives you the number of ticks in the duration’s own unit. It is the escape hatch back to plain integers, so the rule of thumb is to call it as late as possible, ideally only when printing or talking to a C API.

Chrono Literals (C++14)

Writing std::chrono::milliseconds(500) everywhere gets noisy. C++14 added user-defined literals in the std::chrono_literals namespace:

LiteralType
100nsstd::chrono::nanoseconds
100usstd::chrono::microseconds
100msstd::chrono::milliseconds
5sstd::chrono::seconds
10minstd::chrono::minutes
2hstd::chrono::hours
#include <chrono>
#include <thread>

int main() {
    using namespace std::chrono_literals;

    auto total = 1h + 30min + 45s;         // std::chrono::seconds, value 5445
    std::this_thread::sleep_for(500ms);    // instead of std::chrono::milliseconds(500)

    auto half = 1.5s;                      // floating-point rep, 1.5 seconds
}

A few things that are easy to trip over:

  • The namespace is required. Without using namespace std::chrono_literals; (or using namespace std::chrono;, which pulls it in too), 5s fails with “unable to find numeric literal operator operator""s”. Put the using-directive inside a function or a .cpp file, not at namespace scope in a header.
  • s is overloaded. "text"s is a std::string literal from std::string_literals and 5s is seconds. They don’t collide because one takes a string and the other a number, but if you only import std::string_literals you won’t get seconds.
  • Mixed-unit arithmetic yields the finer unit. 1h + 30min + 45s is std::chrono::seconds, not hours. Assigning it to std::chrono::minutes will not compile, which is exactly what you want because 45 seconds would be lost.

The header rule is not just style. A classic breakage: a shared header has using namespace std::chrono; at file scope, and some file that includes it defines its own using days = ...; alias. Under C++17 everything compiles. Switch the project to C++20, std::chrono::days now exists, and every unqualified days in that file becomes ambiguous. Keeping the using-directive inside the function that needs it avoids the whole class of problem.

Practical Examples

Example 1: Time Conversion

#include <chrono>

int main() {
    using namespace std::chrono;

    seconds s(10);

    // Coarser -> finer: implicit, never loses information
    milliseconds ms = s;                       // 10000ms

    // Finer -> coarser: must be explicit, because it may truncate
    seconds s2 = duration_cast<seconds>(ms);   // 10s
    // seconds s3 = ms;                        // error: does not compile
}

The rule is about the type, not the value. seconds s = 1000ms; does not compile even though 1000 ms is exactly one second; the compiler only knows the conversion could lose precision. With a floating-point representation (duration<double>), conversions in either direction are implicit, because nothing gets truncated.

Example 2: Time Operations

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;

    seconds s1(10);
    seconds s2(5);

    auto sum  = s1 + s2;   // 15s
    auto diff = s1 - s2;   // 5s
    auto mul  = s1 * 2;    // 20s
    auto div  = s1 / 2;    // 5s   (duration / scalar -> duration)
    auto ratio = s1 / s2;  // 2    (duration / duration -> plain number)

    if (s1 > s2) {
        std::cout << "s1 is longer\n";
    }
}

Note the last division: dividing two durations cancels the unit and gives you a plain count, which is handy for questions like “how many 100 ms slots fit into this timeout”.

Example 3: Custom duration

#include <chrono>

using fortnights = std::chrono::duration<int, std::ratio<14 * 86400>>;
using weeks_t    = std::chrono::duration<int, std::ratio<604800>>;

int main() {
    weeks_t w(2);
    fortnights f = std::chrono::duration_cast<fortnights>(w);   // 1 fortnight
}

Before C++20 you had to define days and weeks this way. C++20 ships them as std::chrono::days and std::chrono::weeks, so if you’re on C++20 prefer the standard names and avoid defining your own aliases with the same spelling (that’s the ambiguity described above).

Example 4: Timeout Implementation

#include <chrono>
#include <thread>
#include <iostream>

using namespace std::chrono;
using namespace std::chrono_literals;

class Timer {
    steady_clock::time_point start_;
    milliseconds timeout_;

public:
    explicit Timer(milliseconds timeout)
        : start_(steady_clock::now()), timeout_(timeout) {}

    bool expired() const {
        return steady_clock::now() - start_ >= timeout_;
    }

    milliseconds remaining() const {
        auto elapsed = steady_clock::now() - start_;   // steady_clock::duration (usually ns)
        auto left = duration_cast<milliseconds>(timeout_ - elapsed);
        return left > milliseconds::zero() ? left : milliseconds::zero();
    }
};

int main() {
    Timer timer(5s);

    while (!timer.expired()) {
        std::cout << "Remaining: " << timer.remaining().count() << "ms\n";
        std::this_thread::sleep_for(1s);
    }
    std::cout << "Timeout!\n";
}

The duration_cast in remaining() is not optional. steady_clock::now() - start_ is in the clock’s native unit, which is nanoseconds on every mainstream implementation, so timeout_ - elapsed is nanoseconds too. Returning that as milliseconds without a cast is a compile error. That’s the type system doing its job: it forces you to decide that dropping sub-millisecond precision is acceptable here.

Example 5: Rate Limiting

#include <chrono>
#include <thread>
#include <iostream>

using namespace std::chrono;
using namespace std::chrono_literals;

class RateLimiter {
    steady_clock::time_point last_call_;
    milliseconds min_interval_;

public:
    explicit RateLimiter(milliseconds interval)
        : last_call_(steady_clock::now() - interval), min_interval_(interval) {}

    void wait_if_needed() {
        auto elapsed = steady_clock::now() - last_call_;
        if (elapsed < min_interval_) {
            std::this_thread::sleep_for(min_interval_ - elapsed);
        }
        last_call_ = steady_clock::now();
    }
};

int main() {
    RateLimiter limiter(100ms);   // at most ~10 calls per second

    for (int i = 0; i < 5; ++i) {
        limiter.wait_if_needed();
        std::cout << "Call " << i << '\n';
    }
}

Initializing last_call_ to now() - interval lets the first call go through immediately instead of waiting a full interval. sleep_for accepts any duration type, so passing the nanosecond difference directly is fine.

duration_cast, floor, ceil, round

When converting to a coarser integer unit you have four choices. duration_cast exists since C++11; floor, ceil and round were added in C++17.

#include <chrono>

int main() {
    using namespace std::chrono;

    milliseconds ms(1500);

    auto a = duration_cast<seconds>(ms);   // 1s  (truncates toward zero)
    auto b = floor<seconds>(ms);           // 1s  (toward negative infinity)
    auto c = ceil<seconds>(ms);            // 2s
    auto d = round<seconds>(ms);           // 2s  (to nearest, ties to even)

    milliseconds neg(-1500);
    auto e = duration_cast<seconds>(neg);  // -1s
    auto f = floor<seconds>(neg);          // -2s
}

The difference between duration_cast and floor only shows up for negative values, and that is exactly where it causes bugs. If you’re bucketing timestamps relative to some epoch (for example “which one-second bucket does this offset fall into”), duration_cast puts -0.5 s and +0.5 s in the same bucket 0. floor gives the mathematically expected -1 and 0. round uses round-half-to-even, so round<seconds>(2500ms) is 2 s, not 3 s.

Common Issues

Issue 1: Precision Loss

using namespace std::chrono;

milliseconds ms(1500);
seconds s = duration_cast<seconds>(ms);   // 1s (500ms silently lost)

auto s2 = round<seconds>(ms);             // 2s
auto s3 = duration_cast<duration<double>>(ms);   // 1.5 (seconds, as double)

If you need a human-readable “1.5 s”, convert to duration<double> rather than to integer seconds.

Issue 2: Type Mismatch

void func(std::chrono::seconds s) {}

// func(10);                       // error: no implicit int -> seconds
func(std::chrono::seconds(10));    // OK

using namespace std::chrono_literals;
func(10s);                         // OK
func(2min);                        // OK: minutes -> seconds is implicit
// func(1500ms);                   // error: would truncate

Issue 3: Negative duration

using namespace std::chrono;

seconds s1(5);
seconds s2(10);

auto diff = s1 - s2;                       // -5s
std::cout << diff.count() << '\n';         // -5
std::cout << abs(diff).count() << '\n';    // 5 (std::chrono::abs, C++17)

Negative durations are perfectly valid. They show up naturally when you subtract a later time point from an earlier one, so code that computes “time remaining” should clamp to zero as in the Timer example.

Issue 4: Overflow of the representation

The standard only guarantees minimum sizes for the representation (for example, at least 64 bits for nanoseconds, which is about ±292 years). In practice libstdc++, libc++ and MSVC use a 64-bit integer for all of them. Overflow is rarely an issue with the predefined types, but it can happen with a custom duration whose Rep is int and a fine period, or when multiplying a large nanoseconds value. If you build your own duration types, choose a 64-bit Rep unless you have a reason not to.

Defined Time Units

// C++11
std::chrono::nanoseconds
std::chrono::microseconds
std::chrono::milliseconds
std::chrono::seconds
std::chrono::minutes
std::chrono::hours

// C++20
std::chrono::days     // 86400 s
std::chrono::weeks    // 7 days
std::chrono::months   // 1/12 of a year = 2629746 s (average, not calendar)
std::chrono::years    // 365.2425 days = 31556952 s (average Gregorian year)

months and years deserve a warning: they are average lengths. Adding months{1} to a time_point moves it forward by 30.436875 days, which lands at 10:29:06 on some day, not “the same day next month”. For calendar arithmetic, add them to a year_month_day instead, as shown below.

Practical Patterns

Pattern 1: Configurable Timeout

#include <chrono>
#include <thread>

using namespace std::chrono;
using namespace std::chrono_literals;

bool try_connect();   // assumed to exist

class NetworkClient {
    milliseconds timeout_ = 30s;   // seconds -> milliseconds is implicit

public:
    void set_timeout(milliseconds timeout) { timeout_ = timeout; }

    bool connect() {
        const auto deadline = steady_clock::now() + timeout_;
        while (steady_clock::now() < deadline) {
            if (try_connect()) return true;
            std::this_thread::sleep_for(100ms);
        }
        return false;   // timed out
    }
};

// NetworkClient client;
// client.set_timeout(10s);

Computing a deadline time point once and comparing against it is slightly clearer than recomputing elapsed in each iteration, and it maps directly onto APIs like condition_variable::wait_until.

Pattern 2: Retry with Backoff

#include <chrono>
#include <thread>
#include <iostream>
#include <algorithm>

using namespace std::chrono;
using namespace std::chrono_literals;

template <typename Func>
bool retry_with_backoff(Func f, int max_attempts = 5) {
    milliseconds backoff = 100ms;
    const milliseconds max_backoff = 5s;

    for (int attempt = 0; attempt < max_attempts; ++attempt) {
        if (f()) return true;

        std::cout << "Attempt " << attempt + 1 << " failed, waiting "
                  << backoff.count() << "ms\n";
        std::this_thread::sleep_for(backoff);
        backoff = std::min(backoff * 2, max_backoff);   // exponential, capped
    }
    return false;
}

Capping the backoff matters: without a ceiling, doubling from 100 ms passes 100 seconds by the eleventh attempt.

Pattern 3: Performance Measurement

#include <chrono>
#include <iostream>
#include <thread>

using namespace std::chrono;

template <typename Func>
auto measure_time(Func f) {
    auto start = steady_clock::now();
    f();
    auto end = steady_clock::now();
    return duration_cast<microseconds>(end - start);
}

int main() {
    auto elapsed = measure_time([] {
        std::this_thread::sleep_for(std::chrono::milliseconds(20));
    });
    std::cout << "Elapsed: " << elapsed.count() << "us\n";
}

Use steady_clock for this. system_clock follows wall time and can jump backwards when NTP corrects it, which produces negative or wildly wrong measurements. See steady_clock and benchmarking for more.

C++20: Calendar Literals and Dates

C++20 extended <chrono> with calendar types based on Howard Hinnant’s date library, and added two more literals to std::chrono_literals:

LiteralTypeMeaning
2026ystd::chrono::yearA calendar year
29dstd::chrono::dayA day of the month (1 to 31)

Months have no literal; use the constants std::chrono::January … December or month{3}. The / operator glues the pieces into a year_month_day:

#include <chrono>
#include <iostream>

int main() {
    using namespace std::chrono;   // also brings in std::chrono_literals

    year_month_day date = 2026y / March / 29d;   // also valid: March / 29d / 2026y, 29d / March / 2026
    std::cout << date << '\n';                   // 2026-03-29

    // Convert to a time_point with day precision, then add time of day
    sys_days day_start = date;
    auto meeting = day_start + 14h + 30min;      // sys_time<minutes>

    // Difference between dates is a duration in days
    auto diff = sys_days{2026y / March / 29d} - sys_days{2026y / March / 1d};
    std::cout << diff.count() << " days\n";      // 28
}

The biggest pitfall here is the name. 29d is a day, a calendar field, not a days duration. sys_days{date} + 29d doesn’t compile, because you can’t add a day-of-month to a time point. When you want an interval of 29 days, write days{29}.

The second pitfall is calendar months versus duration months, mentioned above:

using namespace std::chrono;

year_month_day jan31 = 2026y / January / 31d;

// Calendar arithmetic on year_month_day: "same day, next month"
year_month_day feb = jan31 + months{1};      // 2026-02-31, which is not a real date
std::cout << feb.ok() << '\n';               // 0
year_month_day fixed = feb.year() / feb.month() / last;   // clamp to 2026-02-28

// Duration arithmetic on a time_point: "+30.436875 days"
auto tp = sys_days{jan31} + months{1};       // not midnight, and not a calendar operation

Adding months to a year_month_day changes the month field and leaves the day alone, so it can produce invalid dates like February 31. The library does not silently roll that over to March 3; it gives you a value whose ok() is false and lets you decide whether to clamp (as above with last) or overflow into the next month (sys_days{feb} normalizes it to March 3). Being forced to choose is the correct behavior, because billing, subscriptions and reporting code disagree on what “one month after January 31” means.

For time zones (zoned_time, current_zone(), "America/New_York"), see Calendar and Timezone.

Compiler support. Everything up to the C++17 sections compiles on any current compiler. The C++20 calendar parts are newer:

  • MSVC has had the full calendar and time zone support since Visual Studio 2019 16.10.
  • libstdc++ (GCC) added the calendar types (year, year_month_day, the y/d literals) in GCC 11, but the operator<< for them and the time zone database only arrived in GCC 14. On GCC 11 to 13, the arithmetic compiles but std::cout << date does not.
  • libc++ (Clang) has the calendar types; time zone support is still being completed in recent releases.

If you are stuck on an older toolchain, Howard Hinnant’s date library provides the same API under namespace date.

FAQ

What’s the difference between duration and time_point?

  • duration: a time interval (“5 seconds”)
  • time_point: a specific moment on a clock (“2026-03-12 14:30:00 UTC”)

Subtracting two time points gives a duration, and adding a duration to a time point gives a time point. Adding two time points is a compile error, because it has no meaning.

Do chrono literals have runtime cost?

No. 500ms is a constexpr call that produces a milliseconds with count 500 at compile time; the generated code is the same as storing the integer.

Can I use a duration as a map key or in a switch?

As a map key, yes: durations are totally ordered. In a switch, no, because a duration is not an integral type; switch on .count() if you really need to.

Where can I learn more?