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

> Discover how Instatic's atomic publishing with two-slot symlinks ensures zero-downtime deployments. Visitors always see complete files with this robust mechanism.

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

---

**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.
- **`current` → `a` | `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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```typescript
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.

### Step 3: Create the Temporary Symlink

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)`.

```typescript
// 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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:**

```typescript
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:**

```typescript
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:**

- [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) – Core implementation of `getActiveSlot`, `getInactiveSlot`, and `swapSlot`.
- [`src/__tests__/server/staticArtefact.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/staticArtefact.test.ts) – Validates atomic swap behavior and crash recovery.
- [`server/publish/publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publicRouter.ts) – Demonstrates runtime reads through the `current` symlink.
- [`docs/features/publisher.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/features/publisher.md) – Architecture documentation for Layer A of the publishing pipeline.

## 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.

### How does the symlink swap maintain atomicity on Windows?

While POSIX systems guarantee atomic `rename(2)` for symlinks, Windows handles this differently. The implementation in [`staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/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 (`current` → `a` 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.