What is the difference between chrono::Duration and std::time::Duration for time calculations?

chrono::Duration supports negative durations and provides rich arithmetic operations, while std::time::Duration is strictly non-negative and offers a minimal API. This fundamental difference shapes when to use each: std::time::Duration is ideal for timeouts and intervals where negative values don't make sense, while chrono::Duration handles calculations that might go negative—subtracting later times, computing time differences, or representing offsets. Understanding this distinction prevents subtle bugs when durations become negative unexpectedly.

std::time::Duration: Non-Negative Only

use std::time::Duration;
 
fn main() {
    // std::time::Duration is always positive or zero
    let five_secs = Duration::from_secs(5);
    let hundred_millis = Duration::from_millis(100);
    
    println!("{:?}", five_secs); // Duration { secs: 5, nanos: 0 }
    
    // Cannot represent negative values
    // let negative = Duration::from_secs(-1); // Compile error!
    // let negative = Duration::new(-1, 0); // Compile error!
    
    // Operations that would go negative saturate at zero
    let small = Duration::from_secs(2);
    let large = Duration::from_secs(5);
    
    // Saturating subtraction
    let result = small.saturating_sub(large);
    println!("2 - 5 = {:?}", result); // 0ns (saturated)
    
    // Checked subtraction returns None
    let checked = small.checked_sub(large);
    println!("Checked: {:?}", checked); // None
    
    // Regular subtraction would panic or need handling
    // This is the key limitation: cannot represent negative durations
}

std::time::Duration panics or saturates on negative results—it cannot represent them.

chrono::Duration: Signed Durations

use chrono::Duration;
 
fn main() {
    // chrono::Duration can be positive or negative
    let positive = Duration::seconds(5);
    let negative = Duration::seconds(-5);
    
    println!("Positive: {:?}", positive); // TimeDelta { secs: 5, nanos: 0 }
    println!("Negative: {:?}", negative); // TimeDelta { secs: -5, nanos: 0 }
    
    // Subtraction can go negative naturally
    let earlier = Duration::seconds(2);
    let later = Duration::seconds(5);
    
    let diff = earlier - later;
    println!("2 - 5 = {:?}", diff); // -3 seconds
    
    // All arithmetic works with signed results
    let a = Duration::seconds(10);
    let b = Duration::seconds(-3);
    let c = Duration::seconds(4);
    
    println!("10 + (-3) = {:?}", a + b); // 7 seconds
    println!("(-3) * 2 = {:?}", b * 2);   // -6 seconds
    println!("(-3) / 2 = {:?}", b / 2);    // -1 second (truncated)
    
    // Absolute value
    println!("abs(-3) = {:?}", b.abs()); // 3 seconds
}

chrono::Duration handles negative values naturally as first-class citizens.

Time Subtraction Differences

use chrono::{DateTime, Utc, Duration as ChronoDuration};
use std::time::{Duration, Instant, SystemTime};
 
fn main() {
    // With chrono DateTime:
    let earlier: DateTime<Utc> = "2024-01-01T10:00:00Z".parse().unwrap();
    let later: DateTime<Utc> = "2024-01-01T12:00:00Z".parse().unwrap();
    
    // Subtracting times gives chrono::Duration (can be negative)
    let diff1 = earlier.signed_duration_since(later);
    println!("Earlier - Later: {:?}", diff1); // -2 hours
    
    let diff2 = later.signed_duration_since(earlier);
    println!("Later - Earlier: {:?}", diff2); // +2 hours
    
    // With std::time:
    let instant1 = Instant::now();
    let instant2 = instant1 + Duration::from_secs(5);
    
    // Duration::from can only be positive
    // instant2.duration_since(instant1) // panic! "supplied instant is later"
    
    // Safe: only subtract earlier from later
    let elapsed = instant2.duration_since(instant1);
    println!("Elapsed: {:?}", elapsed); // 5 seconds
    
    // Must check order first:
    if instant2 > instant1 {
        let safe_elapsed = instant2.duration_since(instant1);
        println!("Safe: {:?}", safe_elapsed);
    }
    
    // SystemTime also panics on negative:
    let sys1 = SystemTime::now();
    let sys2 = sys1 + Duration::from_secs(5);
    
    // This would panic:
    // sys1.duration_since(sys2) // panic! "supplied time is later"
}

chrono returns signed durations; std panics on negative duration calculations.

Conversion Between Duration Types

use chrono::Duration as ChronoDuration;
use std::time::Duration as StdDuration;
 
fn main() {
    // chrono::Duration to std::time::Duration (only if positive)
    let chrono_pos = ChronoDuration::seconds(5);
    let std_pos: StdDuration = chrono_pos.to_std().unwrap();
    println!("Positive conversion: {:?}", std_pos);
    
    // Negative chrono::Duration cannot convert to std::time::Duration
    let chrono_neg = ChronoDuration::seconds(-5);
    let std_result = chrono_neg.to_std();
    println!("Negative conversion: {:?}", std_result); // Err
    
    // std::time::Duration to chrono::Duration (always works)
    let std_duration = StdDuration::from_secs(5);
    let chrono_duration = ChronoDuration::from_std(std_duration);
    println!("std to chrono: {:?}", chrono_duration); // 5 seconds (positive)
    
    // This is useful for interoperability:
    // - Use std::time::Duration for APIs requiring it (async, timeouts)
    // - Use chrono::Duration for calculations
    // - Convert back and forth as needed
}

Convert chrono::Duration to std::time::Duration only when positive; the reverse always works.

Timeout Use Cases

use std::time::Duration;
use chrono::Duration as ChronoDuration;
use tokio::time::timeout;
 
#[tokio::main]
async fn main() {
    // std::time::Duration is appropriate for timeouts
    // Timeouts are always positive by definition
    
    let result = timeout(Duration::from_secs(5), async {
        // Some async operation
        "done"
    }).await;
    
    // chrono::Duration would need conversion:
    let chrono_timeout = ChronoDuration::seconds(5);
    let std_timeout = chrono_timeout.to_std().unwrap(); // Must handle error
    
    let result2 = timeout(std_timeout, async {
        "also done"
    }).await;
    
    // For this use case, std::time::Duration is simpler
    // because timeouts can never be negative
    
    // Common timeout patterns with std::time::Duration:
    tokio::time::sleep(Duration::from_millis(100)).await;
    
    let interval = tokio::time::interval(Duration::from_secs(1));
    // interval.tick().await;
}

For timeouts and sleeps, std::time::Duration is natural and simpler.

Date-Time Arithmetic

use chrono::{DateTime, Utc, TimeZone, Duration};
 
fn main() {
    // chrono::Duration shines for date-time arithmetic
    
    let dt: DateTime<Utc> = Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap();
    
    // Add positive duration
    let later = dt + Duration::hours(3);
    println!("+3 hours: {}", later);
    
    // Add negative duration (subtract)
    let earlier = dt + Duration::hours(-3);
    println!("-3 hours: {}", earlier);
    
    // Duration between dates (can be negative)
    let dt2: DateTime<Utc> = Utc.with_ymd_and_hms(2024, 1, 15, 8, 0, 0).unwrap();
    let diff = dt.signed_duration_since(dt2);
    println!("Duration from dt2 to dt: {:?}", diff); // +2 hours
    
    let diff_reversed = dt2.signed_duration_since(dt);
    println!("Duration from dt to dt2: {:?}", diff_reversed); // -2 hours
    
    // Calculate deadline relative to now
    let now = Utc::now();
    let deadline = now + Duration::days(7);
    let time_remaining = deadline.signed_duration_since(now);
    
    // This might be negative if deadline passed!
    if time_remaining.num_seconds() > 0 {
        println!("Time remaining: {} seconds", time_remaining.num_seconds());
    } else {
        println!("Deadline passed by {} seconds", -time_remaining.num_seconds());
    }
}

chrono::Duration handles bidirectional time calculations naturally.

Precision and Representation

use std::time::Duration as StdDuration;
use chrono::Duration as ChronoDuration;
 
fn main() {
    // Both support nanosecond precision
    let std_nano = StdDuration::new(1, 500_000_000); // 1.5 seconds
    let chrono_nano = ChronoDuration::nanoseconds(1_500_000_000);
    
    // Access methods differ slightly
    
    // std::time::Duration:
    println!("std seconds: {}", std_nano.as_secs());
    println!("std millis: {}", std_nano.as_millis());
    println!("std micros: {}", std_nano.as_micros());
    println!("std nanos: {}", std_nano.as_nanos());
    println!("std subsec millis: {}", std_nano.subsec_millis());
    println!("std subsec micros: {}", std_nano.subsec_micros());
    println!("std subsec nanos: {}", std_nano.subsec_nanos());
    
    // chrono::Duration:
    println!("chrono seconds: {}", chrono_nano.num_seconds());
    println!("chrono millis: {}", chrono_nano.num_milliseconds());
    println!("chrono micros: {}", chrono_nano.num_microseconds());
    println!("chrono nanos: {}", chrono_nano.num_nanoseconds());
    
    // Note: chrono returns signed integers, std returns unsigned
    
    // std::time::Duration internal representation:
    // - secs: u64 (seconds)
    // - nanos: u32 (subsecond nanoseconds, always < 1_000_000_000)
    
    // chrono::Duration internal representation:
    // - Similar, but with signed values
    // - TimeDelta { secs: i64, nanos: i32 }
}

Both provide nanosecond precision but differ in signedness and accessor methods.

Formatting and Display

use std::time::Duration as StdDuration;
use chrono::Duration as ChronoDuration;
 
fn main() {
    // std::time::Duration Debug output
    let std = StdDuration::new(3661, 123_456_789);
    println!("Debug: {:?}", std); // Duration { secs: 3661, nanos: 123456789 }
    
    // No built-in Display
    // println!("{}", std); // Error: Display not implemented
    
    // Manual formatting:
    fn format_std(d: StdDuration) -> String {
        let secs = d.as_secs();
        let hours = secs / 3600;
        let mins = (secs % 3600) / 60;
        let secs = secs % 60;
        let millis = d.subsec_millis();
        format!("{:02}:{:02}:{:02}.{:03}", hours, mins, secs, millis)
    }
    println!("Formatted: {}", format_std(std));
    
    // chrono::Duration has HumanTime output through chrono-humanize (external crate)
    let chrono = ChronoDuration::seconds(3661);
    
    // Basic Debug output
    println!("Debug: {:?}", chrono); // TimeDelta { ... }
    
    // chrono::Duration has built-in helpers:
    println!("Hours: {}", chrono.num_hours());
    println!("Minutes: {}", chrono.num_minutes());
    println!("Seconds: {}", chrono.num_seconds());
    
    // Negative formatting:
    let neg = ChronoDuration::seconds(-3661);
    println!("Negative: {:?}", neg);
    println!("Negative hours: {}", neg.num_hours()); // -1
}

std::time::Duration lacks Display; chrono::Duration provides numeric accessors for custom formatting.

Parsing from Strings

use std::time::Duration;
use chrono::Duration as ChronoDuration;
 
fn main() {
    // std::time::Duration parsing (Rust 1.66+)
    let parsed: Duration = "5s".parse().unwrap();
    println!("Parsed std: {:?}", parsed);
    
    // Supported formats:
    // "5s" - 5 seconds
    // "100ms" - 100 milliseconds
    // "1m30s" - 1 minute 30 seconds (not supported in all versions)
    // "1h" - 1 hour
    
    // chrono::Duration parsing (different approach)
    // chrono parses from numbers, not duration strings
    let chrono_secs = ChronoDuration::seconds(5);
    let chrono_millis = ChronoDuration::milliseconds(100);
    
    // For chrono, you'd typically parse the number first:
    let num: i64 = "5".parse().unwrap();
    let chrono = ChronoDuration::seconds(num);
    
    // Or use chrono's DateTime parsing for full date-times
    // Duration is usually computed from DateTime differences
}

std::time::Duration can parse duration strings; chrono::Duration focuses on programmatic construction.

Performance Characteristics

use std::time::Duration as StdDuration;
use chrono::Duration as ChronoDuration;
 
fn main() {
    // Both are lightweight (a few integers)
    
    // std::time::Duration:
    // - Two fields: u64 secs + u32 nanos
    // - Size: ~12 bytes (may have padding)
    // - Copy is trivial
    
    // chrono::Duration:
    // - Two fields: i64 secs + i32 nanos (or similar)
    // - Size: similar to std
    
    // Performance differences are negligible for most uses
    
    // Key practical difference:
    // - std::time::Duration is in the standard library
    //   - No dependencies
    //   - Works everywhere
    //   - Required by std/async APIs
    
    // - chrono::Duration requires chrono crate
    //   - Richer API
    //   - Interoperates with chrono DateTime
    //   - Additional dependency
    
    // For tight loops with many operations:
    // - Both are very fast
    // - No heap allocation
    // - Arithmetic is just integer math
}

Both are lightweight; the main difference is API richness, not performance.

Practical Conversion Patterns

use std::time::Duration as StdDuration;
use chrono::Duration as ChronoDuration;
use chrono::{DateTime, Utc};
 
// Pattern 1: Use chrono for calculations, convert at boundaries
fn calculate_deadline(start: DateTime<Utc>, offset: ChronoDuration) -> DateTime<Utc> {
    // Calculations may involve negative durations
    start + offset
}
 
// Pattern 2: Convert to std::time for async operations
async fn with_timeout<T>(future: impl std::future::Future<Output = T>, timeout: ChronoDuration) -> Option<T> {
    // Convert chrono::Duration to std::time::Duration for tokio
    let std_timeout = timeout.to_std().ok()?;
    
    tokio::time::timeout(std_timeout, future).await.ok()
}
 
// Pattern 3: Accept both types via traits
trait AsStdDuration {
    fn as_std(&self) -> Option<StdDuration>;
}
 
impl AsStdDuration for StdDuration {
    fn as_std(&self) -> Option<StdDuration> {
        Some(*self)
    }
}
 
impl AsStdDuration for ChronoDuration {
    fn as_std(&self) -> Option<StdDuration> {
        self.to_std().ok()
    }
}
 
fn schedule_task(delay: impl AsStdDuration) {
    if let Some(std_delay) = delay.as_std() {
        println!("Scheduling with delay: {:?}", std_delay);
    } else {
        println!("Cannot schedule with negative duration");
    }
}
 
fn main() {
    schedule_task(StdDuration::from_secs(5));
    schedule_task(ChronoDuration::seconds(5));
    schedule_task(ChronoDuration::seconds(-5)); // "Cannot schedule..."
}

Use chrono::Duration for calculations and convert to std::time::Duration at API boundaries.

When to Use Each

use std::time::Duration as StdDuration;
use chrono::Duration as ChronoDuration;
 
// Use std::time::Duration when:
// 1. Working with async timeouts
// 2. Setting intervals or sleeps
// 3. Interacting with std APIs (Instant, SystemTime)
// 4. Duration is always positive by definition
// 5. Minimal dependencies desired
 
// Use chrono::Duration when:
// 1. Computing time differences (may be negative)
// 2. Working with chrono DateTime
// 3. Need signed arithmetic
// 4. Need to represent offsets from a reference
// 5. Calculating time remaining (deadline - now)
 
fn main() {
    // Timeout: always positive -> std::time::Duration
    let _timeout = StdDuration::from_secs(30);
    
    // Time difference: can be negative -> chrono::Duration
    let diff = ChronoDuration::seconds(5) - ChronoDuration::seconds(10);
    println!("Difference: {:?}", diff); // -5 seconds
    
    // Interoperability:
    // std -> chrono: always works
    let chrono_from_std = ChronoDuration::from_std(StdDuration::from_secs(5));
    
    // chrono -> std: only if positive
    match ChronoDuration::seconds(5).to_std() {
        Ok(std) => println!("Converted: {:?}", std),
        Err(_) => println!("Cannot convert negative duration"),
    }
}

Choose based on whether negative durations are possible in your use case.

Synthesis

Quick reference:

use std::time::Duration as StdDuration;
use chrono::Duration as ChronoDuration;
 
// std::time::Duration:
// - Non-negative only (u64 seconds, u32 nanos)
// - Cannot represent negative time spans
// - Checked/saturating subtraction for underflow
// - Required for timeouts, sleeps, intervals
// - No Display implementation
// - Parse from "5s", "100ms", etc.
// - Works with Instant, SystemTime
 
// chrono::Duration:
// - Signed (i64 seconds, i32 nanos)
// - Negative durations are first-class
// - Direct arithmetic, no overflow concerns
// - Works with DateTime arithmetic
// - to_std() returns Err for negative
// - from_std() always succeeds
// - More numeric accessors
 
// Key operations:
// Subtraction that might go negative:
//   chrono: let diff = a.signed_duration_since(b); // Works, returns signed
//   std: let diff = a.duration_since(b); // Panics if b > a
 
// Conversion:
//   let std = chrono.to_std()?; // Error if negative
//   let chrono = ChronoDuration::from_std(std); // Always works
 
// When negative is impossible (timeouts):
//   Use std::time::Duration directly
 
// When negative is possible (differences):
//   Use chrono::Duration, convert if needed

Key insight: The presence or absence of signedness fundamentally changes how you design time-related code. std::time::Duration forces you to handle negative results explicitly—either checking before subtraction or using checked operations—while chrono::Duration lets negatives flow naturally through your calculations. Use std::time::Duration for the positive-only domain of timeouts and intervals, and chrono::Duration for calculations where time might flow in either direction. Convert at API boundaries using to_std() (which fails for negatives) and from_std() (which always succeeds).