the rename is the deploy
file: journal/the-rename-is-the-deploy.md
title: the rename is the deploy
date: 2026-07-24
tags: deployment, unix, invariants
blurb: Most deploys have a window during which the site is neither the old one nor the new one. That window is optional.
---
the rename is the deploy
Watch a deploy closely and you will find a stretch of time — usually a few hundred milliseconds, sometimes a few minutes — during which the site is neither the version you had nor the version you wanted. Half the files are new. The CSS has been replaced but the HTML that needs it has not. Someone requests a page in exactly that moment and gets a document assembled from two different intentions.
Everyone knows this window exists. Almost nobody measures it, because the fix is assumed to be expensive: blue-green environments, a load balancer, a health check, a rollout controller, a yaml file describing the rollout controller.
It isn't expensive. It's one system call.
the shape of the fix
Build the new site somewhere else entirely. Then move a single pointer.
$build = $buildsRoot.'/'.date('Ymd-His');
mkdir($build, 0755, true);
/* … write every page into $build … */
$tmp = "$root/.public.tmp";
@unlink($tmp);
symlink($build, $tmp);
rename($tmp, $publicLink); /* before: old site. after: new site. */rename() over an existing path is atomic on POSIX filesystems: rename(2) guarantees that a concurrent reader sees either the old entry or the new one, never a missing one and never a half-written one. The web server resolves public on every request. Before the rename it resolves to one finished build; after, to another finished build. There is no third state to be caught in.
The .public.tmp step matters more than it looks. symlink() fails if the target exists, so you cannot create the link directly over the live one — and unlink() followed by symlink() would open exactly the window we are trying to close, a window that is short but wide enough to serve a 404 to whoever is reading at that moment. Creating the link under a scratch name and renaming it into place keeps the whole operation to a single atomic step.
what this buys, precisely
- problem
- A deploy that is observable as a state — half old, half new.
- invariants
- A reader's request resolves to exactly one complete build. A failed build never becomes the live site.
- decisions
- Build to a fresh directory; publish by renaming a symlink; keep the last three builds on disk.
- result
- Deploys stop being events. There is nothing to schedule around and nothing to announce.
The second invariant comes free and is the one people notice later. If the build crashes halfway, it crashes in a directory nobody is serving. The live site does not know a deploy was attempted. You fix the error and run it again — no rollback procedure, because nothing rolled forward.
And rollback, when you do want it, is the same rename pointed at an older directory. Not a redeploy, not a revert commit, not a pipeline run: a rename. The last three builds are still sitting there.
the objection
The usual objection is that this only works for static output, and that real applications have databases and sessions and caches that cannot be swapped by renaming a directory.
That is true and it is also not an argument against doing it where it does work. A schema migration is genuinely hard; serving one consistent set of files is not. Most of what is deployed on any given day is the easy half wearing the difficulty of the hard half as an excuse.
the smaller lesson
The interesting part isn't the symlink. It's that the atomicity we wanted was already sitting in the filesystem, unused, while the industry built increasingly elaborate machinery to approximate it at a higher layer.
Before adding a system, it is worth asking which guarantee you actually need and how far down it already exists. Quite often the answer is: further down than you expected, and free.
resultThis site publishes in one rename(). The window is not small. It is absent.