A scheduler fires an hour early once a year. A billing report duplicates a day because a local date was converted with the wrong timezone database. A log pipeline loses leap-second alignment and offsets an entire spacecraft telemetry stream by a second. None of those bugs are hard to write and all of them are hard to notice — which is why Rust’s time-story changed so much between 2024 and 2026.

For a decade, chrono was the only serious answer. Since then jiff arrived with an explicit design goal of making the wrong thing hard, time matured into the minimal no_std-friendly option, and hifitime proved that even nanosecond-precision leap-second-correct timekeeping belongs in the crate ecosystem. Picking between them is now a real architectural decision.

TL;DR — Quick Verdict

Your situationUseWhy
Normal service code: timestamps, JSON APIs, CRUD appschrono 0.4.45Largest ecosystem, serde support everywhere, works on Rust 1.62+
New project where correctness matters more than familiarityjiffTimezone-aware by default, disambiguation rules for DST gaps, built-in rounding and formatting
Embedded / no_std / minimal dependency surfacetime 0.3Structured formatting, no timezone database, MSRV 1.88
Aerospace, satellites, scientific instrumentation, leap secondshifitime 4.3.1Multiple time scales (TAI, GPST, UTC), leap-second aware, MPL-2.0

Verdict: if you are starting fresh in 2026, use jiff — it handles the DST-gap and ambiguity cases that chrono leaves to you. Stay on chrono if your codebase is large or your dependencies already assume it. Choose time when your binary must stay tiny. Choose hifitime only when “one second off” is a mission failure.

The Comparison (real crate metadata, September 2026)

CrateGitHub starsLast pushLatest versionLicenseMSRVTimezone database
chrono3,9112026-09-070.4.45MIT OR Apache-2.01.62.0via chrono-tz crate
jiff2,9362026-09-120.2 seriesUnlicense OR MIT1.70bundled jiff-tzdb by default
time1,3382026-09-170.3 seriesMIT OR Apache-2.01.88.0none (offsets only)
hifitime5452026-09-094.3.1MPL-2.0modern stablenone (scientific time scales)

Two rows deserve attention. time requires Rust 1.88.0 — older toolchains and long-term-support distributions will not compile it, so check MSRV before adopting it in a fleet of services. And jiff ships a timezone database inside the crate, which removes the classic “the container has stale tzdata” class of bug at the cost of regular dependency updates.

Decision Matrix

Use casePickReason
REST API storing UTC instants, serialized with serdechronoserde impls are everywhere, RFC 3339 helpers built in
Calendar UI across timezones with recurring meetingsjiffZoned type plus explicit DST disambiguation
Firmware or CLI tool that must avoid the tzdb entirelytimeNo timezone database, no runtime data files, no_std capable
Satellite telemetry, GNSS, orbital mechanicshifitimeTAI/GPST/UTC conversions with leap-second tables
Parsing arbitrary user-supplied date stringschrono or jiffBoth have strftime-style parsing; jiff cross-checks against strftime
Long-running service that needs monotonic durationsany + std::time::InstantWall clocks can jump; use Instant for elapsed time

chrono — Still the Default, Still a Trap for Beginners

chrono remains the pragmatic choice: it compiles on old toolchains, has an enormous dependency footprint in the ecosystem, and every tutorial uses it.

1
2
3
4
5
# Cargo.toml
[dependencies]
chrono = { version = "0.4.45", features = ["serde"] }
chrono-tz = "0.10"
serde = { version = "1", features = ["derive"] }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use chrono::{DateTime, NaiveDateTime, Utc};
use chrono_tz::Tz;

fn main() {
    // Instantaneous, unambiguous: always store this
    let now: DateTime<Utc> = Utc::now();
    println!("stored: {}", now.to_rfc3339());

    // Presentation: convert to a real timezone using the chrono-tz database
    let berlin: Tz = "Europe/Berlin".parse().unwrap();
    let local = now.with_timezone(&berlin);
    println!("shown:  {}", local.format("%Y-%m-%d %H:%M:%S %Z"));

    // Parsing needs an explicit offset type, not a naive date
    let parsed = DateTime::parse_from_rfc3339("2026-09-21T14:30:00+02:00").unwrap();
    println!("parsed: {}", parsed.with_timezone(&Utc));

    // NaiveDateTime has NO timezone. Never store one without deciding one.
    let naive: NaiveDateTime = parsed.naive_utc();
    println!("naive (danger): {}", naive);
}

The trap is NaiveDateTime: it is a wall-clock reading with no offset, and every project that stores one eventually has a bug where two servers disagree about what it meant. Store DateTime<Utc> and convert at the edges. Use chrono-tz for named zones and keep that crate updated — it bundles the IANA database, so a stale chrono-tz means wrong historic offsets for regions that changed their rules.

jiff — Built So the Wrong Thing Is Hard

jiff (by the author of ripgrep) takes the opposite approach to chrono’s: types force you to state your intent. Timestamp is a UTC instant, Zoned is an instant plus a timezone, civil::Date is a date without time, and converting between them requires explicit operations. The default build includes the IANA timezone database via a bundled crate, so TZ misconfiguration on the host does not silently change your conversions.

1
2
3
[dependencies]
jiff = "0.2"
serde = { version = "1", features = ["derive"] }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use jiff::{civil, tz::TimeZone, Timestamp, Zoned};

fn main() -> Result<(), jiff::Error> {
    // A UTC instant
    let ts = Timestamp::now();
    println!("instant: {ts}");

    // A zoned timestamp with explicit DST semantics
    let tz = TimeZone::get("America/New_York")?;
    let zoned: Zoned = ts.in_tz(tz)?;
    println!("zoned:   {zoned}");

    // Build a civil datetime, then attach a timezone.
    // 2:30am on a spring-forward day does not exist; jiff must resolve it.
    let meeting = civil::date(2026, 3, 8).at(2, 30, 0, 0);
    let resolved = meeting.to_zoned(tz.clone())?;
    println!("resolved: {resolved}");

    // Arithmetic with rounding, no manual chrono gymnastics
    let next_week = zoned.checked_add(jiff::Span::new().days(7))?;
    println!("next:     {next_week}");
    Ok(())
}

The disambiguation behaviour is the selling point: jiff documents and resolves what happens when a local time is skipped or repeated, instead of silently producing a value that no clock ever displayed. Formatting and parsing use strftime-style strings that jiff tests against the platform’s C library, so %Y-%m-%d means the same thing it means everywhere else.

time — Minimal, Structured, no_std-Friendly

time deliberately omits the timezone database. That is a feature: no tzdata to ship, no file lookups at runtime, and a dependency graph small enough for firmware. Formatting and parsing are described by macros rather than format strings, which the compiler validates at build time.

1
2
[dependencies]
time = { version = "0.3", features = ["macros", "formatting", "parsing", "serde"] }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use time::format_description::well_known::Rfc3339;
use time::macros::format_description;
use time::OffsetDateTime;

fn main() {
    let now = OffsetDateTime::now_utc();
    println!("rfc3339: {}", now.format(&Rfc3339).unwrap());

    // Compile-time checked format description
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]");
    println!("custom:  {}", now.format(&fmt).unwrap());

    let parsed = OffsetDateTime::parse("2026-09-21T14:30:00+02:00", &Rfc3339).unwrap();
    println!("parsed:  {}", parsed.to_offset(time::UtcOffset::UTC));
}

Because there is no named timezone support, time is wrong for anything that must display “3pm in Tokyo on a given date” — you would have to build that mapping yourself. It is right for wire formats, embedded devices, and libraries that want to stay dependency-light. Remember the MSRV 1.88.0 requirement before adding it to a workspace that pins an older toolchain.

hifitime — When a Second Is a Mission Parameter

hifitime models time as it is actually defined in physics and aerospace: multiple time scales, leap seconds, and high precision. It reports itself as “ultra-precise date and time handling in Rust for scientific applications with leap second support”, and version 4.x keeps a leap-second table in the crate.

1
2
[dependencies]
hifitime = "4.3.1"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use hifitime::{Epoch, TimeScale, Unit};

fn main() {
    // Gregorian date in UTC, then convert across time scales
    let epoch_utc = Epoch::from_gregorian_utc(2026, 9, 21, 12, 0, 0, 0);
    let epoch_tai = epoch_utc.to_time_scale(TimeScale::TAI);
    let epoch_gpst = epoch_utc.to_time_scale(TimeScale::GPST);

    println!("UTC:  {epoch_utc}");
    println!("TAI:  {epoch_tai}");
    println!("GPST: {epoch_gpst}");

    // Explicit units, no accidental millisecond/second mixups
    let duration = 90.0 * Unit::Second;
    println!("+90s: {}", epoch_utc + duration);
}

Use this crate when leap seconds, relativistic time scales or nanosecond alignment are part of the specification. Do not use it for a web API — you will fight the ecosystem, because serde support and formatting conventions assume civilian time libraries. It is licensed MPL-2.0, which is fine for linking but worth a compliance check if your legal team keeps a strict allowlist.

Making Time Reproducible in CI and Containers

Most production time bugs are environment bugs. Three practices prevent nearly all of them:

1
2
3
4
5
6
7
8
9
# Pin the timezone database your service actually uses
FROM rust:1.89-slim
RUN apt-get update && apt-get install -y --no-install-recommends tzdata ca-certificates && \
    rm -rf /var/lib/apt/lists/*
ENV TZ=UTC
WORKDIR /app
COPY . .
RUN cargo build --release --locked
CMD ["./target/release/scheduler"]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Test the DST boundaries explicitly instead of hoping they never break
#[cfg(test)]
mod dst_tests {
    use chrono::TimeZone;
    use chrono_tz::America::New_York;

    #[test]
    fn spring_forward_gap_is_not_silently_wrong() {
        // 2026-03-08 02:30 local does not exist in New York.
        // chrono resolves it forward; assert the behaviour your product promised.
        let resolved = New_York.with_ymd_and_hms(2026, 3, 8, 2, 30, 0).single();
        assert!(resolved.is_some(), "document the resolution rule you rely on");
    }
}
  1. Force TZ=UTC in every container and convert to local time only at the presentation layer.
  2. Pin the timezone database — either the chrono-tz version in Cargo.lock (commit it, and build with --locked) or the tzdata package version in your image, so an OS update cannot change historical offsets behind your back.
  3. Test both DST transitions for every timezone your product claims to support. Spring-forward gaps and autumn repeats are where the interesting bugs live.

If your pipeline also parses human-written date strings, the cross-language datetime parsing comparison covers the parser side, including how chrono sits next to Python’s dateutil and the JVM’s Joda-Time. If you are publishing your own time helper crate, the self-hosted Rust crate registry guide shows how to host it internally. And when your service talks TLS to other systems, the Rust TLS library comparison is a good reminder that certificate validity windows are just more dates to get wrong.

Common Pitfalls

Storing NaiveDateTime. A wall clock without an offset is not a timestamp. Store UTC instants; convert when you display.

Using wall-clock time for durations. Utc::now() can jump backwards when NTP corrects the clock. Measure elapsed time with std::time::Instant, which is monotonic.

Ignoring DST gaps and repeats. In spring-forward, a local time may never have existed; in autumn, it happens twice. jiff forces you to pick a resolution rule — chrono will happily give you something that looks plausible.

Trusting Local in containers. Local::now() depends on the TZ environment variable and available tzdata files. In a slim container with the variable unset, you get UTC and never notice until someone reads a report in the wrong day.

Assuming leap seconds never matter. Civilian libraries ignore them, which is correct for most applications and fatal for satellite or GNSS work.

Mixing serde representations between services. Decide once whether your JSON carries RFC 3339 strings or epoch numbers, then assert it in tests. Round-tripping through a number loses the original offset.

Forgetting MSRV. time needs Rust 1.88.0 and jiff needs 1.70; adding either to a workspace pinned to an older toolchain breaks the build in CI but not on the developer’s machine.

Calendar arithmetic on durations. Adding “one month” to 31 January is ambiguous — every library answers differently. Use explicit calendar operations from jiff (Span) or document the chrono behaviour you rely on.

FAQ

Should I switch from chrono to jiff in 2026? Switch for new projects, and migrate progressively for existing ones. jiff makes intent explicit — you separate instants, zoned timestamps and civil dates — and it resolves DST gaps and repeated local times with documented rules instead of leaving the decision to you. chrono remains perfectly serviceable and has broader ecosystem support, so a large codebase with many DateTime<Utc> values has little to gain from an urgent rewrite.

Which Rust crate is best for timezone-aware scheduling? jiff, because Zoned carries the timezone with the instant and forces a disambiguation choice when a local time does not exist or occurs twice. chrono with chrono-tz is the established alternative, but you must handle the gap and repeat cases yourself, and you must keep the chrono-tz data updated for correct historic offsets.

Does the time crate support named timezones like Europe/Berlin? No, and that is deliberate. time handles UTC offsets only and ships no timezone database, which keeps it small enough for embedded and no_std targets. If you need named zones, use chrono with chrono-tz or jiff with its bundled database and treat time as your wire-format layer.

What is the minimum supported Rust version for these crates? As of September 2026: chrono 0.4.45 requires Rust 1.62.0, jiff requires 1.70, time requires 1.88.0 and hifitime 4.3.1 tracks a modern stable toolchain. If your organisation pins an older compiler for compliance reasons, chrono is the safest choice of the four.

How do I test code that depends on the current time? Never call the clock directly inside business logic — inject it behind a trait or function parameter so tests can supply a fixed instant. Then add explicit regression tests for both DST transitions in every timezone you support, plus one test asserting your JSON representation stays stable, so a dependency upgrade cannot silently change the wire format.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com