How to Get File Metadata (Size, Modified Time) in Rust

Retrieve file size and modification time in Rust using std::fs::metadata and its len and modified methods.

When the file content isn't enough

You are building a backup script that skips unchanged files. Or a development server that reloads when a configuration file updates. In Python, you grab os.path.getmtime and move on. In Rust, the standard library hands you std::fs::metadata. The return type looks heavier. The size is a simple number. The modification time comes wrapped in a SystemTime that refuses to print with a basic format string. The API forces you to confront what file metadata actually is. It is operating system state, not just a number. System calls fail. Clocks jump backward. Symlinks redirect you to entirely different inodes. Rust's types reflect those realities instead of hiding them behind convenience functions.

What metadata actually holds

Think of a file as a storage box. The content is the stuff inside. The metadata is the label on the outside. The label records the weight, the date it was last opened, who owns it, and whether it is locked. Rust bundles this label into a std::fs::Metadata struct. The struct does not guess. It asks the operating system for the truth, and it hands you exactly what the OS returns. If the OS does not track a particular field, the method returns an error. If the clock was adjusted by a user or an NTP sync, the timestamp reflects that jump. The language treats time as a measurement of reality, not a guaranteed linear counter.

Minimal example

Here is the smallest working case: fetching a path, extracting the byte count, and grabbing the modification timestamp.

use std::fs;

/// Fetch and display basic file metadata.
fn main() -> std::io::Result<()> {
    // metadata() triggers a system call. It returns a Result because
    // the file might be missing, inaccessible, or a directory.
    let meta = fs::metadata("notes.txt")?;

    // len() reads a cached field from the struct. No system call occurs here.
    let size = meta.len();

    // modified() queries the OS for the last write time.
    // Some filesystems do not track this, so it returns a Result.
    let mtime = meta.modified()?;

    println!("Size: {size} bytes");
    // SystemTime lacks Display. Debug formatting shows the raw epoch value.
    println!("Modified: {mtime:?}");
    Ok(())
}

Convention aside: The community defaults to fs::symlink_metadata when processing arbitrary paths. fs::metadata follows symbolic links and returns information about the target file. fs::symlink_metadata inspects the link itself. Using the symlink variant prevents accidental traversal and keeps your tool predictable.

Walkthrough of what happens

When you call fs::metadata, Rust translates your path into a platform-specific system call. On Linux, this is typically stat. On Windows, it is GetFileAttributesEx. The call can fail for dozens of reasons. The file might vanish between your check and the kernel's execution. You might lack read permissions on the parent directory. That is why the return type is Result<Metadata, Error>. The ? operator propagates the failure cleanly.

Extracting the size with len() is cheap. The kernel already packed the byte count into the Metadata struct during the initial call. You are just reading a field from memory.

Extracting the modification time with modified() behaves differently across platforms. On some systems, it reads a pre-filled field. On others, it triggers a secondary query. It always returns a Result<SystemTime, Error> because filesystems vary wildly in how they track timestamps.

Printing SystemTime with a standard format string fails at compile time. The type does not implement the Display trait. You will hit E0277 (the trait bound std::fmt::Display is not satisfied) if you try println!("{}", mtime). Use {:?} for quick debugging, which dumps the internal epoch representation. For human-readable dates, the standard library deliberately stops short. Time zones, leap seconds, and calendar reforms are notoriously complex. The ecosystem delegates formatting to dedicated crates.

Treat the Result types as a contract with the operating system. The kernel does not owe you a timestamp. Handle the error path before you assume the data exists.

Realistic example: checking file age

A common requirement is checking whether a file changed recently. You can measure the gap between the modification timestamp and the current moment using SystemTime::elapsed(). This method returns a Result<Duration, SystemTimeError>. The error triggers if the modification time appears to be in the future. This happens when the system clock jumps backward after the file was written.

use std::fs;
use std::time::Duration;

/// Check if a configuration file was updated within the last hour.
fn is_recent(path: &str) -> std::io::Result<bool> {
    let meta = fs::metadata(path)?;
    let mtime = meta.modified()?;

    // elapsed() calculates time since the event.
    // It fails if the clock jumped backward, making the event appear future.
    // unwrap_or(Duration::ZERO) treats future timestamps as immediate.
    let duration = mtime.elapsed().unwrap_or(Duration::ZERO);

    // Compare the raw seconds against a one-hour threshold.
    Ok(duration.as_secs() < 3600)
}

fn main() -> std::io::Result<()> {
    if is_recent("config.json")? {
        println!("Config changed recently. Reloading.");
    } else {
        println!("Config is stale. Using cached version.");
    }
    Ok(())
}

Convention aside: unwrap_or(Duration::ZERO) is the standard pattern for cache invalidation and file watchers. If the clock jumps backward, treating the file as freshly modified is the safe default. Your application reloads unnecessarily once, rather than ignoring a critical update. If you need strict audit logging, handle the SystemTimeError explicitly and record the anomaly.

Pitfalls and compiler errors

File metadata hides several landmines. The compiler will not save you from logical mistakes, but it will catch type mismatches.

Symlink traversal is the most common trap. fs::metadata follows links. If you check a symbolic link pointing to a massive database file, len() returns the database size, not the link size. Your logic will silently misclassify the path. Use fs::symlink_metadata when you need to inspect the link itself.

Directory size is another illusion. Calling len() on a folder returns the size of the directory entry on disk, typically 4096 bytes on ext4. It never sums the contents. If you need the total size of a tree, you must walk the directory recursively. The walkdir crate handles the recursion and error handling efficiently.

Time formatting requires external help. SystemTime does not implement Display. You get E0277 if you attempt standard string interpolation. You also get E0308 (mismatched types) if you try to cast a SystemTime directly to a u64. The standard library keeps time minimal. Reach for chrono or time when you need calendar math or human-readable output.

Permissions are platform-dependent. permissions().readonly() checks the read-only flag, but the underlying bits differ between Unix and Windows. Setting permissions with set_readonly() clears write bits on Unix but sets a single attribute on Windows. If you need fine-grained control, import the OS-specific extension traits. The base API is a lowest-common-denominator abstraction.

Trust the Result types. They exist because the filesystem is an external, mutable system. Handle the errors explicitly.

Decision matrix

Use fs::metadata when you want information about the actual file at the path, following symbolic links to their targets. Use fs::symlink_metadata when you need information about the link itself, or when you want to avoid following links for security and correctness. Use metadata.len() for the byte count of a single file. Use the walkdir crate when you need the cumulative size of a directory tree. Use SystemTime when you need wall-clock timestamps like creation dates or modification logs. Use Instant when you need to measure elapsed time for performance benchmarks, as it is monotonic and immune to clock adjustments. Use the chrono crate when you need to format timestamps for display or parse date strings. Use std::time::Duration when you need to represent a span of time between two events.

Treat the distinction between metadata and symlink_metadata as a boundary condition. Symlinks redirect your assumptions. Reach for symlink_metadata by default unless you explicitly want to follow links. Don't assume len() sums a tree. Walk the directory if you need total size. The compiler protects you from negative durations. Handle the Result from elapsed() and duration_since(). Time can jump backward. Your code should expect it.

Where to go next