# How Instatic's Two-Slot Symlink Swap Enables Atomic Publishing

> Discover how Instatic's two-slot symlink swap ensures atomic publishing. Learn how this method guarantees visitors never see incomplete pages.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-30

---

**Instatic guarantees atomic page publishing by maintaining two directory slots (`a` and `b`) and using an atomic `rename(2)` operation to flip a `current` symlink between them, ensuring visitors never observe partially written files.**

The Instatic static site generator solves the half-written file problem through a robust two-slot symlink swap mechanism implemented in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts). This design leverages POSIX atomicity guarantees to ensure every deployment is an all-or-nothing operation, completely isolating readers from the write process.

## The Two-Slot Directory Model

Instatic’s publishing system relies on a three-part directory structure under `uploads/published/`:

| Directory | Purpose |
|-----------|---------|
| `uploads/published/a` | Holds a complete set of static artefacts for one publish version. |
| `uploads/published/b` | Holds a complete set of static artefacts for the next publish version. |
| `uploads/published/current` | A symlink that points to either `a` or `b`; all runtime reads resolve through this path. |

As noted in the source comment at line 8 of [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts), the `current` symlink acts as the "visitor router" that the kernel resolves on every `open(2)` call. This ensures each HTTP request sees a consistent snapshot of either the old or new version, never a mixture.

## Step-by-Step Atomic Publishing Flow

### 1. Determine the Inactive Slot

The system first identifies which slot is not currently active. The `getInactiveSlot()` function (line 178 in [`staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/staticArtefact.ts)) inspects the `current` symlink and returns `'a'` or `'b'` depending on which directory is *not* being served.

### 2. Write to the Inactive Slot

All newly generated static files are copied into the determined inactive slot (e.g., `uploads/published/a`). This step never touches the active slot, allowing readers to continue accessing the current version without interference.

### 3. Create a Temporary Symlink

Before flipping traffic, the code creates a temporary symlink using `symlink(targetSlot, tmpPath)` (line 286), resulting in a `current.tmp` file that points to the newly populated slot.

### 4. Execute the Atomic Swap

The critical operation occurs with `rename(tmpPath, currentPath)` (described at line 258). On POSIX systems, renaming a symlink is atomic—a single kernel operation that either succeeds completely or fails without side effects. This instantly directs all new requests to the fresh slot while any in-flight requests continue reading from the old directory handle.

### 5. Preserve the Old Slot

After the swap completes, the previous slot remains intact on disk. This enables instant rollbacks and ensures no reader encounters a half-written directory state, as the old files remain fully available until the next publishing cycle overwrites that slot.

## Why This Approach Guarantees Atomicity

**Atomic `rename(2)`**. The POSIX `rename` system call ensures the symlink points to either the old or new slot, never an intermediate or corrupted state. If the system crashes during the rename, the symlink要么 retains its old value, maintaining consistency.

**Directory-level isolation**. Each slot is a completely separate directory tree. Writers populate one tree while readers consume the other, eliminating race conditions between file creation and HTTP response generation.

**Graceful Windows fallback**. For non-POSIX platforms, the implementation first removes any stale temporary symlink before executing the rename, preserving the same atomicity guarantees across operating systems.

## Implementation Code Examples

**Flipping the active slot** demonstrates the public API:

```typescript
import { swapSlot, getActiveSlot, getInactiveSlot } from '@server/publish/staticArtefact';
import { uploadsDir } from '@server/config';

async function publishNewVersion(staticFiles: Buffer[]) {
  // Step 1: Identify the safe slot to write to
  const inactive = await getInactiveSlot(uploadsDir);
  
  // Step 2: Write files to uploads/published/${inactive} ...
  await writeToSlot(uploadsDir, inactive, staticFiles);
  
  // Step 3 & 4: Atomically swap the symlink
  await swapSlot(uploadsDir, inactive);
  
  // Verify the swap succeeded
  const nowLive = await getActiveSlot(uploadsDir);
  console.log(`Now serving from slot: ${nowLive}`); // 'a' or 'b'
}

```

**Reading through the symlink** in the request handler:

```typescript
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

export async function servePage(path: string) {
  // The kernel resolves `current` atomically on each open()
  const fullPath = join(uploadsDir, 'published', 'current', path);
  return await readFile(fullPath, 'utf8');
}

```

## Key Source Files

Understanding the implementation requires examining these specific locations:

- **[`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts)**: Contains the core logic for `getActiveSlot()`, `getInactiveSlot()`, and `swapSlot()`, including the atomic rename operation at line 258.
- **[`src/__tests__/server/staticArtefact.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/staticArtefact.test.ts)**: Validates the swap semantics in lines 151-158, ensuring the `current` symlink flips correctly and the old slot remains accessible.
- **[`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts)**: Demonstrates how production traffic reads files through the `current` symlink, resolving paths via the kernel’s atomic symlink handling.

## Summary

- **Two-slot isolation**: Instatic maintains separate `a` and `b` directories to isolate read and write operations.
- **Atomic symlink swap**: The `rename(2)` system call provides an instantaneous, non-interruptible switch between versions.
- **Zero-downtime deployments**: Readers always see complete file sets, never partial writes or mixed states.
- **Cross-platform safety**: Special handling ensures atomicity on both POSIX systems and Windows.

## Frequently Asked Questions

### How does the symlink swap remain atomic on Windows?

On Windows, the implementation removes any existing `current.tmp` symlink before creating the new one, then performs the rename. This sequence ensures that even without POSIX atomic rename semantics for symlinks, the system never enters an inconsistent state where the `current` pointer is corrupted.

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

If the process fails before the symlink swap occurs, the `current` symlink remains pointing to the old slot. Readers continue receiving the previous version, and the next publish attempt simply overwrites the incomplete data in the inactive slot before attempting the swap again.

### Can readers access the old slot after a atomic swap completes?

Yes. The old slot remains fully populated and accessible through its direct path (`uploads/published/a` or `b`), though production traffic routes through `current`. This enables instant rollbacks by manually restoring the symlink if the new version contains critical errors.

### How does the router know which slot to serve without checking filesystem state?

The router does not track slot state directly. It always reads from `uploads/published/current`, and the kernel resolves this symlink at the moment of each `open()` call. This design delegates atomicity to the filesystem layer, eliminating the need for the application to manage synchronization locks or state checks.