Your 'Atomic' Deploys Probably Aren't Atomic

Published: (January 12, 2026 at 05:13 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for Your

If you’re using symlink swaps for zero‑downtime deployments, you’ve probably written something like this:

ln -sfn releases/20260112 current

Looks atomic. It’s not.

The Bug

Run that command through strace:

symlink("releases/20260112", "current") = -1 EEXIST
unlink("current") = 0                    
symlink("releases/20260112", "current") = 0

Between unlink and the second symlink the current symlink doesn’t exist. Under load, some requests hit that gap and get ENOENT. Your “zero‑downtime” deploy just caused downtime.

The Linux Fix

The well‑documented solution is:

ln -s releases/20260112 .tmp/current.$$
mv -T .tmp/current.$$ current

Create a temporary symlink, then use mv -T to atomically replace the target. The -T flag forces mv to call rename(2), which is atomic on POSIX filesystems.

The macOS Problem

BSD mv lacks -T and follows symlinks differently—if current points to a directory, mv .tmp/current.$$ current moves the temp symlink into that directory instead of replacing it. The Capistrano community has long known this and works around it with Ruby, but most tools either accept the race condition on macOS or require Linux for development.

A Different Approach

A portable solution works on both Linux and macOS by using Python’s os.replace(), which directly invokes rename(2) on all POSIX systems.

detect_platform() {
    if mv --version 2>/dev/null | grep -q 'GNU'; then
        printf 'linux'
    else
        printf 'bsd'
    fi
}

activate_release() {
    local tmp_link=".tmp/current.$$"
    ln -s "releases/$release_id" "$tmp_link"

    if [[ "$(detect_platform)" == "linux" ]]; then
        mv -T "$tmp_link" "current"
    else
        python3 -c "import os; os.replace('$tmp_link', 'current')"
    fi
}

Platform detection checks for GNU coreutils (mv --version) rather than relying on uname, handling edge cases like Homebrew GNU coreutils on macOS.

Proving the Race Condition

A simple script demonstrates the bug:

#!/bin/bash
mkdir -p releases/v1 releases/v2
echo "v1" > releases/v1/version
echo "v2" > releases/v2/version
ln -s releases/v1 current

# Reader loop in background
(
  for i in {1..10000}; do
    cat current/version 2>/dev/null || echo "ENOENT"
  done
) > reads.log &

reader_pid=$!

# Writer loop – rapidly swap symlink
for i in {1..1000}; do
  ln -sfn releases/v1 current
  ln -sfn releases/v2 current
done

wait $reader_pid

errors=$(grep -c ENOENT reads.log || true)
echo "Errors: $errors / 10000 reads"

On a typical system you’ll see 10–50 ENOENT errors per run. Using the atomic approach eliminates them.

The Full Script

The author wrapped the logic into a single‑file deployment script that provides:

  • Atomic symlink swap on Linux and macOS
  • Directory‑based locking with stale PID detection
  • Automatic rollback on SIGINT/SIGTERM
  • State‑machine cleanup that knows whether to rollback or just remove temporary files

The script has no dependencies beyond Bash and Python 3.
GitHub:

What It Doesn’t Do

  • Shared directories – No Capistrano‑style shared folder symlinking
  • Remote deployment – Wrap it in ssh/rsync yourself
  • Release pruning – Add a cron job if needed
  • Service restarts – Use your init system

One thing, done right.

FAQ

Why not just use Capistrano/Deployer?
They’re great if you’re already in Ruby/PHP. This script can be dropped into any CI pipeline.

Why not containers?
Not everyone runs Kubernetes. VMs, bare metal, and edge devices still exist.

Python is a dependency.
Python 3 ships with macOS and virtually every Linux distro. It’s as ubiquitous as Bash.

What about renameat2() with RENAME_EXCHANGE?
That’s Linux 3.15+ with glibc 2.28+. It provides a true atomic swap but isn’t portable.

Does this work on NFS?
No. rename(2) atomicity guarantees don’t hold on network filesystems.

Further Reading

Back to Blog

Related posts

Read more »

Hello, Newbie Here.

Hi! I'm falling back into the realm of S.T.E.M. I enjoy learning about energy systems, science, technology, engineering, and math as well. One of the projects I...