# How the Instatic Publisher Generates Static HTML with Atomic Two-Layer Caching

> Instatic publisher generates static HTML using atomic caching. Discover how its unique system prevents partial files and supports dynamic rendering for optimal performance.

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

---

**The Instatic publisher combines atomic slot-based file storage with a version-stamped in-memory LRU cache to generate and serve static HTML, ensuring visitors never see partially-written files while supporting dynamic rendering.**

Instatic, an open-source static site generator maintained at CoreBunch/Instatic, achieves zero-downtime publishing through a sophisticated two-layer caching architecture. This system separates pre-rendered static artefacts from dynamic page generation, using atomic file operations and publish-version tracking to guarantee cache coherency across the entire request lifecycle.

## Overview of the Two-Layer Architecture

The caching system operates through complementary mechanisms defined in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) and [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts). **Layer A** maintains a dual-slot filesystem structure that enables atomic deployments, while **Layer B** provides an in-memory LRU cache for dynamic content with automatic invalidation based on a global publish version stored in [`server/publish/publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishState.ts).

Together, these layers ensure that the [`publicRouter.ts`](https://github.com/CoreBunch/Instatic/blob/main/publicRouter.ts) entry point can serve fully-rendered HTML instantly from disk for static routes, while falling back to cached dynamic generation when necessary—all without exposing intermediate states to visitors.

## Layer A: Slot-Aware Static Artefacts

The static artefact system uses an **A/B slot pattern** to guarantee atomic publishes. Instead of overwriting active files, the system writes to an inactive slot and swaps the symlink atomically.

### Preparing the Inactive Slot

The `prepareInactiveSlot()` function in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) identifies which slot (`a` or `b`) is currently inactive and wipes it clean while the active slot continues serving traffic. This ensures that visitors cannot access partially-written artefacts during the publishing process.

### Atomic Write Operations

When writing content, `writeArtefact()` creates a temporary file with a `*.tmp` extension in the target slot directory, then uses the atomic `rename(2)` system call to move it into place. This ensures that readers never encounter half-written HTML files, as the filesystem guarantees that `open(2)` calls will see either the old file or the new file, never an intermediate state.

### Zero-Downtime Slot Swapping

The `swapSlot()` function creates a temporary symlink `current.tmp` pointing to the newly populated slot, then atomically renames it over the `current` symlink. Because the `current` symlink always points to a fully-populated slot, visitors experience either the old or new version of the site, never a mixed state.

### Reading with Race-Condition Handling

The `readArtefact()` function follows the `current` symlink in a single `open(2)` syscall and implements retry logic to survive transient races—such as when a slot wipe occurs between symlink resolution and file opening. If the artefact is missing or the URL is unsafe, the function returns `null`, triggering a fallback to the render cache layer.

## Layer B: In-Memory LRU Render Cache

For dynamic routes that cannot be pre-baked, Instatic uses an in-memory cache that respects the global publish lifecycle.

### Version-Stamped Cache Keys

Cache entries are keyed by a combination of `urlPath` and `queryString` joined with a NUL separator to prevent collisions. Each entry records the `publishVersion` that was current when rendering began, as exposed by [`publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/publishState.ts). This versioning ensures that cached content from previous publishes never bleeds into new deployments.

### Single-Flight Rendering with getOrRender()

The `getOrRender()` function in [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts) implements a **single-flight pattern**: when a cache miss occurs, concurrent requests for the same URL wait for a single rendering operation rather than triggering duplicate work. The factory function renders the page, and the result is cached only if the publish version has not changed during rendering, preventing stale HTML storage.

### Cache Invalidation and Eviction

The `peek()` method returns cached responses only if the entry's version matches the current global version, promoting the entry in LRU order when accessed. A configurable `RENDER_CACHE_MAX_ENTRIES` setting caps the map size, evicting the oldest entries when the limit is reached. The `getStats()` function exposes hits, misses, and current size for observability.

## Complete Publishing Workflow

### Full Site Publishing

A complete publish orchestrated by [`server/publish/publishSite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishSite.ts) follows this atomic workflow:

```typescript
import { prepareInactiveSlot, writeArtefact, swapSlot } from '@/server/publish/staticArtefact'

async function publishAll(pages: {url: string; html: string}[], uploadsDir: string) {
  // 1️⃣ Prepare the inactive slot (`a` or `b`)
  const { slot, slotDir } = await prepareInactiveSlot(uploadsDir)

  // 2️⃣ Write each page into the inactive slot using atomic temp files
  for (const {url, html} of pages) {
    await writeArtefact(slotDir, url, html)
  }

  // 3️⃣ Atomically make the new slot live via symlink swap
  await swapSlot(uploadsDir, slot)
}

```

### Incremental Updates

For single-page updates without a full slot swap, `updateArtefactInPlace()` uses the same temporary-file-plus-rename pattern to modify files in the active slot atomically:

```typescript
import { updateArtefactInPlace } from '@/server/publish/staticArtefact'

await updateArtefactInPlace('/tmp/uploads', '/blog/my-post', '<html>…</html>')

```

### Dynamic Route Handling

When `readArtefact()` returns null for missing static files, the system falls back to the render cache:

```typescript
import { getOrRender, peek } from '@/server/publish/renderCache'

async function handleRequest(urlPath: string, query: string) {
  const key = { urlPath, queryString: query }

  // Try the fast in-memory path first
  const cached = peek(key)
  if (cached) return cached

  // Miss – render once and cache the result with single-flight protection
  return await getOrRender(key, async () => {
    const html = await renderPageFromSnapshot(urlPath, query)
    return html ? { body: html, headers: {}, status: 200 } : null
  })
}

```

## Summary

- **Atomic file operations** in [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts) use A/B slots and `rename(2)` to guarantee visitors never see partially-written HTML.
- **Zero-downtime deployment** occurs through symlink swapping, where `current` always points to a complete slot.
- **Version-stamped caching** in [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts) ensures dynamic content respects publish boundaries via the global `publishVersion`.
- **Single-flight rendering** prevents thundering-herd problems by deduplicating concurrent render requests for the same URL.
- **Automatic invalidation** happens lazily when `peek()` detects version mismatches, eliminating the need for explicit cache clearing during deployment.

## Frequently Asked Questions

### What happens if a visitor requests a page during a slot swap?

Visitors either receive files from the old slot or the new slot, but never a mixture or partially-written content. Because `swapSlot()` uses an atomic `rename(2)` operation on the `current` symlink, filesystem reads are isolated to one complete slot or the other.

### How does Instatic prevent serving stale dynamic content after a new publish?

The render cache in [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts) stamps each entry with the `publishVersion` active when rendering started. The `peek()` function compares this value against the current global version from [`publishState.ts`](https://github.com/CoreBunch/Instatic/blob/main/publishState.ts) and returns `null` for stale entries, forcing a re-render on the next request.

### Why does Instatic use two slots instead of overwriting files in place?

Overwriting files in place risks serving incomplete content to visitors during large writes. The two-slot system allows `prepareInactiveSlot()` to populate an entire site structure atomically, then switch traffic instantly via symlink without risking file corruption or partial states in the active serving directory.

### What is the single-flight pattern and why is it important?

The single-flight pattern in `getOrRender()` ensures that when multiple concurrent requests miss the cache for the same URL, only one rendering operation executes while others wait for the result. This prevents redundant CPU work and database queries during traffic spikes, as implemented in [`server/publish/renderCache.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/renderCache.ts).