How Instatic's Atomic Publishing Mechanism Works: Zero-Downtime Deployments with Two-Slot Symlinks

Instatic guarantees zero-downtime deployments by using a two-slot directory system with an atomic symlink swap that ensures visitors never see partially written files.

Instatic implements an atomic publishing mechanism that eliminates race conditions during static site updates. The CoreBunch/Instatic repository uses a dual-slot directory architecture combined with POSIX atomic rename operations to ensure every file request resolves to a complete, consistent snapshot. This approach prevents the "half-written page" problem that plagues traditional static site generators during deployment.

The Two-Slot Directory Model

Instatic maintains three entries inside uploads/published/:

  • a/ – Contains a complete set of static artefacts for one publish version.
  • b/ – Contains a complete set of static artefacts for the next publish version.
  • currenta | b – A symbolic link that points to whichever slot is actively serving traffic.

All runtime readers access files through the current symlink (for example, current/index.html). The kernel resolves this symlink on every open(2) call, ensuring each request sees a consistent snapshot of whichever slot was active at the moment the file handle was opened.

This architecture is documented in the header comment of server/publish/staticArtefact.ts at line 8: "current -> a | b (symlink; visitor router reads through this)".

The Atomic Publishing Process

The publishing pipeline in server/publish/staticArtefact.ts follows a five-step sequence to achieve true atomicity.

Step 1: Identify the Inactive Slot

The system determines which slot is safe to write to by checking where current currently points. The getInactiveSlot() function (implemented at line 178) returns 'a' or 'b' depending on the active symlink target.

const inactive = await getInactiveSlot(uploadsDir);
// Returns the slot NOT currently pointed to by 'current'

Step 2: Write to the Inactive Slot

The new static artefacts are written into the inactive slot (e.g., uploads/published/a/). This operation never touches the active slot, meaning readers continue to receive the previous complete version without interference.

Before flipping traffic, the system creates current.tmp as a symlink to the newly populated slot. According to the implementation at line 286, this uses symlink(targetSlot, tmpPath).

// Creates uploads/published/current.tmp -> a (or b)
await symlink(targetSlot, tmpPath);

Step 4: Execute the Atomic Swap

The critical moment uses POSIX rename(2) to atomically replace current with current.tmp. As implemented at line 258 in staticArtefact.ts, the rename(tmpPath, currentPath) operation guarantees that from the kernel's perspective, the symlink either points to the old slot or the new slot—never an intermediate state or broken link.

On Windows, the code implements a safe fallback sequence that removes any stale temporary symlink before renaming, preserving the same atomic guarantee on non-POSIX platforms.

Step 5: Preserve the Previous Slot

After the rename succeeds, the previous slot remains untouched on disk. This enables instantaneous rollbacks and ensures that any file handles opened before the swap continue to read valid data from the old slot until they close.

Why This Guarantees Atomicity

The mechanism relies on three core properties:

  • Atomic rename(2) – The kernel executes this as a single indivisible operation. Either the symlink update succeeds completely, or it fails without side effects.
  • Directory-level isolation – Each slot is a fully separate directory tree. Writing to a/ never modifies files in b/, preventing partial overwrites.
  • Symlink resolution timing – Because the kernel resolves the current symlink at the moment of open(2), requests that began before the swap naturally complete using the old slot, while new requests immediately see the new slot.

The "swapSlot semantics" test in src/__tests__/server/staticArtefact.test.ts (lines 151-158) validates this behavior, verifying that swapSlot correctly flips the active pointer while leaving the previous slot intact.

Implementation Examples

Flipping the active slot:

import { swapSlot, getActiveSlot } 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 through the stable symlink:

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
}

Key source files:

Summary

  • Instatic uses a two-slot system (a/ and b/) with a current symlink to isolate writes from reads.
  • The atomic publishing mechanism relies on POSIX rename(2) to swap symlinks without intermediate states.
  • Readers never see partial content because file handles opened before the swap continue using the old slot until closed.
  • The implementation includes Windows compatibility through a safe fallback sequence that maintains atomic guarantees.
  • All publish operations are recoverable because the previous slot remains available for instant rollback.

Frequently Asked Questions

What happens if the publish process crashes while writing to the inactive slot?

Readers remain unaffected because they access files exclusively through the current symlink, which still points to the previously completed slot. The incomplete data in the inactive slot is simply overwritten on the next publish attempt.

While POSIX systems guarantee atomic rename(2) for symlinks, Windows handles this differently. The implementation in staticArtefact.ts uses a safe sequence that first removes any stale current.tmp before creating the new symlink and performing the rename, ensuring readers never encounter a broken or missing symlink.

Why use two slots instead of renaming a single directory?

Renaming a directory that is actively being read can cause "file not found" errors for requests in progress. The two-slot model ensures that files remain accessible at stable paths through the current symlink, while the actual slot directories (a/ and b/) serve as isolated staging areas that can be fully prepared before going live.

Can readers see a mix of old and new files during the swap?

No. Because the swap happens at the symlink level (currenta or b), and the kernel resolves this symlink atomically at open(2) time, any individual file handle is opened against either the complete old slot or the complete new slot. There is no mechanism for a single request to read files from both versions simultaneously.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →