How Instatic's Two-Slot Symlink Swap Enables Atomic Page Publishing
Instatic guarantees atomic page publishing by maintaining two directory slots (a and b) under uploads/published/ and atomically flipping a current symlink between them using a POSIX rename(2) operation, ensuring readers never see partially written files.
In the CoreBunch/Instatic repository, the two-slot symlink swap is the core mechanism that makes static page publishing an all-or-nothing operation. This design ensures that site visitors always see a complete, consistent version of the published files and never encounter a partially written directory. The implementation lives primarily in server/publish/staticArtefact.ts and leverages the atomicity of the rename(2) system call to cut over traffic instantly.
The Two-Slot Directory Model
The architecture relies on three elements inside uploads/published/:
uploads/published/a— A complete directory of static artefacts for one publish.uploads/published/b— A complete directory of static artefacts for the next publish.uploads/published/current— A symlink that points to whichever slot is active. All runtime readers resolve files through this symlink.
A comment at the top of server/publish/staticArtefact.ts (line 8) outlines this model: "current -> a | b (symlink; visitor router reads through this)". Because the kernel resolves the symlink on every open(2), each HTTP request sees a consistent snapshot of whichever slot was active at the moment the request began.
The Atomic Publishing Pipeline
Step 1 — Identify the Inactive Slot
Before writing any new files, the publisher determines which slot is safe to overwrite. The getInactiveSlot() function in server/publish/staticArtefact.ts (line 178) inspects the current symlink and returns the opposite slot, either 'a' or 'b'. This guarantees that the actively served directory remains untouched during the write phase.
Step 2 — Write Artefacts to the Inactive Slot
With the inactive slot identified, Instatic copies the freshly generated static pages into that directory—for example, uploads/published/a. Since current still points to the other slot, existing readers continue to see the old, fully published version while the new files land on disk without interfering with live traffic.
Step 3 — Stage a Temporary Symlink
Once the inactive slot contains a complete set of files, the code creates a temporary symlink named current.tmp that points to the newly filled slot. In server/publish/staticArtefact.ts (line 286), the implementation calls symlink(targetSlot, tmpPath) to prepare the new pointer without affecting the live current symlink.
Step 4 — Atomically Flip the Pointer
The critical moment uses rename(tmpPath, currentPath) as implemented around line 258 of server/publish/staticArtefact.ts. On POSIX systems, renaming a symlink is an atomic kernel operation; it either succeeds completely or fails without side effects, ensuring that current switches instantly from the old slot to the new one. The implementation also includes a Windows fallback that first removes a stale temporary symlink before renaming, preserving the same all-or-nothing guarantee on non-POSIX platforms.
Step 5 — Leave the Previous Slot Intact
After the rename succeeds, the old slot remains on disk in its complete state. This enables near-instant rollbacks and eliminates any race condition where a reader might see a half-written directory. The swap is literally atomic: every request resolves current/<file> at the kernel level, so the moment the rename completes, subsequent opens see the new slot, while in-flight requests continue reading from the old slot.
Why the Two-Slot Symlink Swap Works
Several properties of filesystems and the Instatic implementation combine to make this reliable:
- Atomic
rename(2)— The Linux and macOS kernels handle symlink renames atomically, meaningcurrentnever points to an invalid or mixed state. - Directory-level isolation — Each slot is a completely separate directory tree, so writing never corrupts the files that readers currently have open.
- Kernel-resolved symlinks — Because the symlink is resolved on every
open(2), there is no user-space caching that could leak an intermediate state to visitors. - Graceful Windows fallback — The special-case logic for Windows removes stale temporary links before renaming, preventing atomicity violations on non-POSIX hosts.
The "swapSlot semantics" test in src/__tests__/server/staticArtefact.test.ts (lines 151–158) validates that swapSlot correctly flips the active slot and that the previously live directory stays intact.
Publishing and Reading Through the Symlink
Flipping the active slot during a publish:
import { swapSlot, getInactiveSlot } from '@server/publish/staticArtefact';
// Determine the inactive slot (the one we will write to)
const inactive = await getInactiveSlot(uploadsDir);
// Write your new static files into `uploads/published/${inactive}` …
// Atomically make the new slot live
await swapSlot(uploadsDir, inactive);
// After the call, getActiveSlot points to the freshly published slot
const nowLive = await getActiveSlot(uploadsDir); // 'a' or 'b'
Reading a page through the stable symlink endpoint:
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
// The router always reads through the `current` symlink
export async function readPublishedPage(path: string) {
const fullPath = join(uploadsDir, 'published', 'current', path);
return await readFile(fullPath, 'utf8'); // kernel resolves symlink atomically
}
Core Source Files
server/publish/staticArtefact.ts— Contains the core protocol implementation, includinggetActiveSlot,getInactiveSlot, andswapSlot.src/__tests__/server/staticArtefact.test.ts— Exercises atomic swap behavior, slot discovery, and crash recovery scenarios such as theswapSlotsemantics test.server/publish/publicRouter.ts— Demonstrates how the production server resolves requests by reading files through thecurrentsymlink.docs/features/publisher.md— Provides the high-level architectural overview of the publishing pipeline, including how the two-slot symlink swap serves Layer A.
Summary
- Instatic uses a two-slot symlink swap to guarantee that published pages are never served in a partially written state.
- The design depends on two physical directories,
aandb, and a singlecurrentsymlink that points to the active slot. getInactiveSlot()identifies the safe target for new writes, whileswapSlot()performs an atomicrename(2)to cut over traffic.- Readers always resolve files through
current, so the kernel's atomic symlink semantics ensure each request sees a consistent snapshot. - The old slot remains available after a cutover, enabling immediate rollback if a new publish must be reverted.
Frequently Asked Questions
How does Instatic's two-slot symlink swap prevent serving partially published pages?
By writing every new publish into whichever directory slot current does not point to, Instatic ensures that live readers never touch incompletely written files. The final cutover happens through a single atomic rename(2) on the symlink, so at no point can current resolve to a mixed or half-written directory.
What happens if a publish fails midway through writing to the inactive slot?
If the write phase aborts or crashes before swapSlot is called, the current symlink simply continues pointing to the previously completed slot. Visitors remain unaffected, and the publisher can retry by wiping the incomplete inactive slot and starting the write fresh.
Is the symlink swap atomic on Windows as well as Linux?
Yes. While POSIX kernels guarantee atomic symlink renames natively, server/publish/staticArtefact.ts (around line 258) includes a Windows-specific fallback that removes any stale temporary symlink before performing the rename. This non-POSIX path preserves the same all-or-nothing guarantee.
How can I read published pages through the current symlink?
Any runtime reader—such as the request handler in server/publish/publicRouter.ts—should build paths under uploads/published/current/. Because the kernel resolves the symlink on every open(2), each request automatically reads from whichever slot is active at that exact instant.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →