How to Use Temporary Files and Directories in Rust (tempfile crate)

Use the tempfile crate to create self-cleaning temporary files and directories in Rust.

When the file must vanish

You are building a tool that updates a configuration file. The user runs the command. Your tool writes the new config. A power outage hits halfway through. The config is now half-written and corrupted. The user can't start their app. You need a way to write the data safely, verify it, and only then replace the old file. If anything goes wrong, the old file stays intact. Temporary files solve this.

You write to a temporary location. The temp file is isolated. Once the data is complete and valid, you move the temp file to the final destination. If the process crashes, the temp file is left behind but the real config is untouched. The user's system remains stable.

Rust makes this pattern easy and safe. The tempfile crate provides types that tie the file's lifecycle to Rust variables. Create the variable, the file appears. Drop the variable, the file vanishes. The compiler guarantees the cleanup. You never write a cleanup function. You never forget to delete a temp file.

Ownership meets the filesystem

Rust's ownership system applies to files just like it applies to data in memory. A value has one owner. When the owner goes out of scope, the value is dropped. tempfile uses this rule for filesystem objects.

NamedTempFile is a struct that owns a temporary file. It holds the file handle and the path. When the NamedTempFile is dropped, it calls the operating system to delete the file. TempDir does the same for directories. It deletes the directory and all its contents recursively when dropped.

This approach eliminates two common bugs. First, resource leaks. In languages without automatic cleanup, developers often forget to delete temp files after errors, filling up disk space. In Rust, the drop happens automatically even if a function returns early due to a panic or an error. Second, double-deletes. If you try to delete a file that's already gone, you get an error. With ownership, the file is deleted exactly once, when the single owner drops.

The crate also handles cross-platform differences. Temporary directories live in different places on different systems. Linux uses /tmp. Windows uses %TEMP%. macOS uses /var/folders. tempfile queries the OS for the correct location. You don't need to write platform-specific code.

The minimal setup

Add tempfile to your Cargo.toml. The crate is the standard solution for this task. The Rust standard library does not yet include temporary file support, so tempfile is the community convention.

use tempfile::{NamedTempFile, tempdir};

fn main() -> std::io::Result<()> {
    // Creates a file in the system temp directory.
    // The name is unique and random.
    // The file is empty and ready to write.
    let file = NamedTempFile::new()?;

    // Creates a directory in the system temp directory.
    // Returns a TempDir that owns the directory.
    let dir = tempdir()?;

    // Access the paths.
    // These are borrowed references tied to the owners.
    let file_path = file.path();
    let dir_path = dir.path();

    // Use the paths for reading or writing.
    // When main ends, file and dir drop.
    // The OS deletes the file and directory automatically.
    Ok(())
}

The code creates a file and a directory. Both are deleted when main returns. The ? operator propagates errors. If the temp directory is full or permissions are wrong, the function returns an error instead of panicking.

What happens under the hood

When you call NamedTempFile::new(), the crate calls the OS to create a file with a unique name. On Unix, this usually involves mkstemp. On Windows, it uses CreateFile with a random name. The crate ensures the name is unique to avoid collisions.

The returned NamedTempFile struct contains a std::fs::File and a PathBuf. The File keeps the file handle open. This is important. Keeping the handle open prevents other processes from deleting the file while you are using it. It also allows you to write to the file using standard Rust I/O traits.

When the NamedTempFile is dropped, the Drop implementation closes the file handle and calls std::fs::remove_file on the path. The file is gone.

NamedTempFile is not Clone. You cannot make a copy of it. This is intentional. If you could clone a NamedTempFile, you would have two variables pointing to the same file. When the first one drops, it deletes the file. The second one would then hold a reference to a deleted file. Any attempt to write would fail. Rust prevents this by enforcing exclusive ownership. If you need to share the path, you borrow it with path(). If you need to transfer ownership, you move the value.

Real-world pattern: atomic updates

The most common use case for temporary files is atomic updates. You write to a temp file, then move it to the final location. This ensures the final file is never partially written.

The tempfile crate provides persist for this. persist takes the NamedTempFile and moves it to a new path. If the target path already exists, persist overwrites it. On Unix, this is atomic. The rename happens instantly. On Windows, atomicity depends on the filesystem, but persist handles the best available mechanism.

use std::fs::File;
use std::io::Write;
use tempfile::NamedTempFile;

/// Writes a config file atomically.
/// If the write fails, the original config is untouched.
fn update_config(path: &std::path::Path, data: &str) -> std::io::Result<()> {
    // Create a temp file in the same directory as the target.
    // This ensures the move is atomic on the same filesystem.
    let mut temp = NamedTempFile::new_in(path.parent().unwrap())?;

    // Write the new data.
    temp.write_all(data.as_bytes())?;

    // Sync to disk to ensure data is flushed.
    temp.flush()?;

    // Move the temp file to the final path.
    // This consumes the NamedTempFile.
    // If this fails, the temp file is still deleted on drop.
    temp.persist(path)?;

    Ok(())
}

The function creates a temp file in the same directory as the target. This is a convention. Moving files across filesystems is not atomic. By keeping the temp file on the same filesystem, the rename operation is guaranteed to be atomic on Unix. The persist call moves the file. If persist succeeds, the NamedTempFile is consumed and no longer exists. The drop does not run. The file remains at the new path. If persist fails, the NamedTempFile is still alive. When the function returns an error, the drop runs and deletes the temp file. The target path is unchanged.

Pitfalls and compiler traps

Temporary files introduce lifetime issues. The path returned by path() is a borrow. It lives as long as the NamedTempFile. If you try to return the path, the compiler rejects the code.

fn bad_example() -> std::io::Result<&'static std::path::Path> {
    let file = NamedTempFile::new()?;
    // Error E0515: cannot return value referencing local data.
    // The file is dropped at the end of the function.
    // The path would point to a deleted file.
    Ok(file.path())
}

The compiler error E0515 tells you the path references local data. The file variable is local. It will be dropped. The path would be invalid after the function returns.

To fix this, you have two options. Use into_temp_path() to convert the NamedTempFile into a TempPath. TempPath is a type that owns the deletion logic but does not hold an open file handle. You can return a TempPath and the file will be deleted when the TempPath drops.

use tempfile::NamedTempFile;

fn good_example() -> std::io::Result<tempfile::TempPath> {
    let file = NamedTempFile::new()?;
    // Convert to TempPath.
    // The file handle is closed.
    // The path is returned.
    // The file is deleted when the TempPath drops.
    Ok(file.into_temp_path())
}

TempPath is lighter than NamedTempFile. It does not keep the file handle open. This matters on Windows. Windows locks files that have open handles. If you pass a NamedTempFile path to a process that needs exclusive access, the process might fail because Rust still holds the handle. TempPath avoids this by closing the handle.

Another pitfall is permissions. Temporary files often inherit restrictive permissions. On some systems, the temp directory has strict access controls. If you need to share the temp file with another user or process, you may need to adjust permissions. The tempfile crate does not set permissions by default. You must use std::fs::set_permissions if needed.

Cross-platform temp directory locations can also cause issues if you hardcode paths. Always use tempfile to create temp files. Never assume /tmp exists.

Decision: picking the right tool

Use NamedTempFile::new() when you need a quick scratch file with no specific name requirements and you plan to write to it immediately. Use NamedTempFile::builder() when you need a specific suffix, prefix, or to create the file in a custom directory. Use tempdir() when you need a temporary directory for multiple files or a complex directory structure. Use persist() when you want to save the temp file to a permanent location atomically. Use into_temp_path() when you need to pass the path to a function that does not take a NamedTempFile but you still want auto-cleanup. Use keep() when you want to prevent deletion on drop, effectively leaking the file intentionally for debugging or hand-off to another process.

Convention aside: the community prefers persist over manual fs::rename. persist handles the NamedTempFile lifecycle correctly. It avoids leaving the temp file behind if the rename fails. It also handles the conversion from NamedTempFile to a permanent file in one step.

Convention aside: use NamedTempFile::new_in(dir) to create temp files in the same directory as the target. This ensures atomic moves on the same filesystem. Moving across filesystems is slower and not atomic.

Convention aside: tempfile is the standard. Do not write your own temporary file logic. The crate handles edge cases like unique naming, cross-platform paths, and cleanup on panic. Rolling your own solution introduces bugs.

Trust the borrow checker on paths. If the compiler complains about lifetimes, the path is tied to a temp file that will vanish. Use TempPath or persist to manage the lifecycle explicitly.

Where to go next