perl.gg / snippets

Atomic File Write

2026-03-18

This page shows how to write a file in Perl without ever leaving a partial or empty copy behind. You will learn the temp file plus rename pattern, why it works, and how to wrap it in a production ready subroutine.

The solution is four lines of Perl and about forty years of filesystem wisdom:

open my $fh, '>', "$file.tmp.$$" or die "Can't write $file.tmp.$$: $!"; print $fh $data; close $fh or die "Can't close $file.tmp.$$: $!"; rename("$file.tmp.$$", $file) or die "Can't rename to $file: $!";
Write to a temp file. Rename it into place. That is the whole trick.

Part 1: WHY DIRECT WRITES ARE DANGEROUS

The obvious approach:
open my $fh, '>', $file or die "Can't write $file: $!"; print $fh $data; close $fh;
That > truncates the file immediately on open. Before you have written a single byte, the old content is gone. If your process dies between open and close, you have either an empty file or a partial one.
Timeline of disaster: open '>' $file --> old content: GONE. 0 bytes. print chunk 1 --> partial data print chunk 2 --> still partial *** CRASH *** --> chunks 1-2 of N (service restarts, reads garbage, bad day)
Even without crashes, there is a race condition. Another process reads the file while you are writing it. No crash needed. Just bad timing.

Part 2: THE TEMP FILE PATTERN

Split the operation into two phases. Create the new content in a temp file. Then atomically swap it into place.
my $tmp = "$file.tmp.$$"; open my $fh, '>', $tmp or die "Can't write $tmp: $!"; print $fh $data; close $fh or die "Can't close $tmp: $!"; rename($tmp, $file) or die "Can't rename $tmp -> $file: $!";
The $$ variable is the current process ID. It makes the temp filename unique per process. Two instances writing the same file will not clobber each other temp files.
Process 1 writes to: config.yml.tmp.4821 Process 2 writes to: config.yml.tmp.4822 Both rename to: config.yml Last rename wins, but both files are always complete.
No partial files. No truncation of the original until the replacement is fully written and ready.

Part 3: WHY RENAME() IS ATOMIC

Perl rename() calls the C library rename(2), which on POSIX systems is an atomic operation within the same filesystem. It either fully succeeds or fully fails. There is no intermediate state.
Before rename(): config.yml <-- old, complete content config.yml.tmp.$$ <-- new, complete content After rename(): config.yml <-- new, complete content (temp file is gone)
At no point does config.yml contain partial data. Any process that opens the file gets either the complete old version or the complete new version. Never a mix. Never empty.

This guarantee comes from the filesystem, not from Perl. Perl just gives you clean access to it.

Part 4: THE CROSS-FILESYSTEM GOTCHA

One critical requirement: the temp file and the target must be on the same filesystem. rename() across filesystem boundaries fails.
# FAILS: /tmp and /etc are different filesystems rename("/tmp/config.tmp.$$", "/etc/myapp/config.yml"); # $! will be "Invalid cross-device link"
The fix is simple. Put the temp file next to the target:
# WORKS: same directory, same filesystem my $tmp = "/etc/myapp/config.yml.tmp.$$";
This is why the pattern uses "$file.tmp.$$" instead of File::Spec->tmpdir(). Same directory guarantees same filesystem.

Part 5: CHECKING CLOSE() FOR ERRORS

Notice the or die on close. This is not paranoia. This is essential.
close $fh or die "Can't close $tmp: $!";
Buffered I/O means print might succeed even if the disk is full. The data sits in a buffer in memory. The actual write to disk happens at close when the buffer flushes. If the flush fails, close returns false and sets $!.

If you skip this check, you might rename a file that was not fully written. You have atomically replaced your good file with a bad one. The pattern saved you from crashes but you defeated it by ignoring the write error.

Always check close(). It is the most commonly skipped error check in file I/O and the one that matters most here.

Part 6: PRESERVING PERMISSIONS

rename() does not handle permissions. The temp file has your umask defaults. If the original had specific permissions, they are gone.

Fix it before the rename:

my $tmp = "$file.tmp.$$"; open my $fh, '>', $tmp or die "Can't write $tmp: $!"; print $fh $data; close $fh or die "Can't close $tmp: $!"; # Copy permissions from the original if (-e $file) { my @stat = stat($file); chmod $stat[2] & 07777, $tmp; } rename($tmp, $file) or die "Can't rename $tmp -> $file: $!";
The & 07777 masks off the file type bits, keeping only permissions. If you are running as root and need ownership preserved too:
chown $stat[4], $stat[5], $tmp; # uid, gid

Part 7: SIGNAL HANDLING AND CLEANUP

What if your process catches a SIGTERM while writing? The temp file sticks around like a ghost.
my $tmp = "$file.tmp.$$"; my $cleanup = sub { unlink $tmp; exit 1; }; local $SIG{INT} = $cleanup; local $SIG{TERM} = $cleanup; local $SIG{HUP} = $cleanup; open my $fh, '>', $tmp or die "Can't write $tmp: $!"; print $fh $data; close $fh or die "Can't close $tmp: $!"; rename($tmp, $file) or die "Can't rename $tmp -> $file: $!";
The local scopes the signal handlers. When the surrounding block exits, original handlers are restored. Interrupted? Temp file cleaned up, original untouched.

For belt and suspenders safety, wrap it in an eval:

eval { open my $fh, '>', $tmp or die "write: $!"; print $fh $data; close $fh or die "close: $!"; rename($tmp, $file) or die "rename: $!"; }; if ($@) { unlink $tmp; die "Atomic write failed for $file: $@"; }
Exception? Temp file removed. Original intact. Clean failure.

Part 8: FILE::TEMP ALTERNATIVE

Perl core File::Temp module generates temp files with random names, slightly more collision resistant than $$:
use File::Temp qw(tempfile); my ($fh, $tmp) = tempfile("${file}.XXXX", UNLINK => 0); print $fh $data; close $fh or die "close: $!"; rename($tmp, $file) or die "rename: $!";
The XXXX becomes random characters. UNLINK => 0 prevents auto deletion (since we are renaming it).

The $$ pattern is simpler for most cases though, and has better debuggability. "What is config.yml.tmp.4821? Oh, PID 4821 was writing a new config." Clear provenance.

Part 9: THE PRODUCTION SUBROUTINE

Here is the whole thing wrapped in a reusable function:
sub atomic_write { my ($file, $data) = @_; my $tmp = "$file.tmp.$$"; my $cleanup = sub { unlink $tmp; exit 1; }; local $SIG{INT} = $cleanup; local $SIG{TERM} = $cleanup; eval { open my $fh, '>', $tmp or die "open: $!"; if (ref $data eq 'CODE') { $data->($fh); # callback for large writes } else { print $fh $data; } close $fh or die "close: $!"; # Preserve permissions if (-e $file) { my @st = stat($file); chmod $st[2] & 07777, $tmp; } rename($tmp, $file) or die "rename: $!"; }; if ($@) { unlink $tmp; die "atomic_write($file): $@"; } return 1; }
Usage:
# Simple string write atomic_write("/etc/myapp/config.yml", $yaml_string); # Callback for large writes (avoids building giant string in memory) atomic_write("/var/data/report.csv", sub { my $fh = shift; for my $row (@rows) { print $fh join(",", @$row), "\n"; } });

Part 10: REAL-WORLD CONFIG UPDATE

Putting it all together. Read a YAML config, modify it, write it back atomically:
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; use YAML::Tiny; my $config_file = '/etc/myapp/config.yml'; # Read current config my $yaml = YAML::Tiny->read($config_file) or die "Can't read $config_file: " . YAML::Tiny->errstr; # Modify it $yaml->[0]{last_updated} = time(); $yaml->[0]{maintenance} = 0; # Write atomically my $tmp = "$config_file.tmp.$$"; eval { $yaml->write($tmp) or die "YAML write: " . YAML::Tiny->errstr; my @st = stat($config_file); chmod $st[2] & 07777, $tmp; rename($tmp, $config_file) or die "rename: $!"; }; if ($@) { unlink $tmp; die $@; } say "Config updated atomically.";
This pattern is everywhere. Vim uses it. Package managers use it. Every serious daemon that writes state to disk uses temp plus rename. It is not clever. It is not flashy. It is the kind of boring code that keeps you from getting paged at 3 AM.
+------------------+ +------------------+ | Write to temp | ---> | rename() into | | $file.tmp.$$ | | place | +------------------+ +------------------+ | | (crash here = (atomic = temp file left, no partial original intact) files ever) .--. |o_o | |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/ Write safe. Sleep well.
perl.gg