Instatic Publisher In-Memory LRU Cache and Version Bumps: Implementation Guide
Instatic uses a fast in-memory LRU cache keyed by URL path, query parameters, and publish version to store rendered HTML, automatically invalidating the entire cache via bumpPublishVersion() whenever content changes to ensure stale pages are never served.
The CoreBunch/Instatic repository implements a high-performance publishing pipeline that relies on an in-memory LRU cache to serve pre-rendered HTML instantly. This article examines how the server/publish/renderCache.ts module manages cache entries using a native JavaScript Map and how server/publish/publishState.ts coordinates version bumps to invalidate cached content atomically.
How the Instatic LRU Cache Works
Instatic’s publishing pipeline treats the in-memory cache as Layer B, sitting between incoming HTTP requests and the expensive render pipeline (Layer A). When a request arrives, the system constructs a composite key from the URL path, canonical query string, and current publish version to look up cached HTML.
Cache Structure and Key Composition
Each cache entry is keyed by the tuple (urlPath, canonicalQuery, publishVersion). According to the source implementation in server/publish/renderCache.ts, the actual cache key is constructed as a pipe-delimited string:
const cacheKey = `${urlPath}|${query}|${version}`;
The cache stores the full HTML output along with the version number used at insertion time. This design ensures that any change to the global publish version automatically invalidates all existing entries, as the version component of the key will no longer match.
LRU Eviction Strategy Using Map
The cache is implemented using a plain JavaScript Map where insertion order tracks recency. This approach provides O(1) lookups while maintaining LRU semantics without external dependencies.
When the cache exceeds its configurable capacity of 1000 entries, the implementation automatically evicts the oldest entry (the first item in the Map’s iteration order). On a cache hit, the entry is deleted and re-inserted to move it to the most-recent position:
// Promote entry to most-recent position
renderCache.delete(cacheKey);
renderCache.set(cacheKey, cached);
This simple promotion strategy ensures frequently accessed pages remain in memory while rarely used entries naturally age out.
Publish Version Management and Cache Invalidation
Central to Instatic’s consistency guarantees is the global publish version—a monotonically increasing integer that acts as a logical clock for the entire site.
bumpPublishVersion() and Global Versioning
The server/publish/publishState.ts file exports bumpPublishVersion(), which performs two critical operations atomically: it increments the global version counter and clears the entire LRU cache. This ensures that once a publish completes, all subsequent requests generate fresh HTML rather than serving stale content from memory.
// server/publish/publishState.ts
let publishVersion = 0;
export function bumpPublishVersion(): number {
publishVersion++;
// Invalidate the whole cache – all entries become stale
renderCache.clear();
return publishVersion;
}
export function getPublishVersion(): number {
return publishVersion;
}
The getPublishVersion() function provides read-only access to the current version, allowing render functions to construct cache keys without triggering invalidation.
Automatic Cache Clearing on Publish
Every successful publish operation wraps its execution with withPublishLock(), which automatically invokes bumpPublishVersion() upon completion. As implemented in server/publish/publishSite.ts, this guarantees that the cache invalidation happens exactly once per publish, regardless of how many files changed:
export async function withPublishLock<T>(fn: () => Promise<T>): Promise<T> {
// acquire exclusive lock …
const result = await fn();
bumpPublishVersion(); // version bump + cache reset
// release lock …
return result;
}
Concurrency Handling and In-Flight Renders
To prevent redundant computation under load, the cache implementation tracks in-flight renders using a separate lookup map. When multiple concurrent requests target the same uncached URL, only the first trigger initiates the render pipeline; subsequent requests await the same promise and share the resulting HTML.
This deduplication mechanism works alongside the LRU cache to ensure that even during cache misses, the system does not waste CPU cycles rendering identical pages simultaneously.
Complete Code Implementation
The following patterns demonstrate the integration between the render cache and publish state modules as found in the CoreBunch/Instatic source code.
Cache Lookup with LRU Promotion
// server/publish/renderCache.ts
import { getPublishVersion } from './publishState';
import { renderPage } from './renderTreeWalk';
const renderCache = new Map<string, { html: string; version: number }>();
const MAX_CACHE_SIZE = 1000;
export async function getRenderedHtml(urlPath: string, query: string) {
const version = getPublishVersion();
const cacheKey = `${urlPath}|${query}|${version}`;
// Try LRU cache
const cached = renderCache.get(cacheKey);
if (cached) {
// Promote entry to most-recent position
renderCache.delete(cacheKey);
renderCache.set(cacheKey, cached);
return cached.html;
}
// No cache entry – render the page
const html = await renderPage(urlPath, query);
// Store in LRU (evicts oldest if capacity exceeded)
if (renderCache.size >= MAX_CACHE_SIZE) {
const firstKey = renderCache.keys().next().value;
renderCache.delete(firstKey);
}
renderCache.set(cacheKey, { html, version });
return html;
}
Version Bump Integration
// server/publish/publishState.ts
import { withPublishLock } from './publishLock';
let publishVersion = 0;
/** Increment the global publish version and clear the LRU cache. */
export function bumpPublishVersion(): number {
publishVersion++;
// Invalidate the whole cache
renderCache.clear();
return publishVersion;
}
/** Return the current publish version (no side-effects). */
export function getPublishVersion(): number {
return publishVersion;
}
/** Wrap a publish operation with automatic version bumping. */
export async function withPublishLock<T>(fn: () => Promise<T>): Promise<T> {
// Acquire exclusive lock implementation...
const result = await fn();
bumpPublishVersion();
// Release lock...
return result;
}
Summary
- Instatic’s Layer B cache uses a native JavaScript
Mapto implement an O(1) LRU cache with a default capacity of 1000 entries, storing HTML keyed by URL path, query string, and publish version. - Automatic invalidation occurs via
bumpPublishVersion()inserver/publish/publishState.ts, which clears the entire cache whenever content changes, guaranteeing strong consistency. - LRU maintenance happens through delete-reinsert operations on cache hits, ensuring frequently accessed pages remain in memory while the oldest entries evict automatically.
- Concurrency safety is achieved by tracking in-flight renders to prevent duplicate work, while
withPublishLock()ensures version bumps happen exactly once per publish operation.
Frequently Asked Questions
How does Instatic handle cache invalidation during a publish?
Instatic handles cache invalidation atomically through the bumpPublishVersion() function defined in server/publish/publishState.ts. When a publish completes, this function increments a global version counter and calls renderCache.clear(), wiping the entire LRU cache. Since cache keys include the publish version, any pre-existing entries become unreachable, ensuring stale HTML is never served.
What is the default cache capacity and how does eviction work?
The default cache capacity is 1000 entries, as implemented in server/publish/renderCache.ts. The system uses a JavaScript Map where insertion order tracks recency. When the cache exceeds capacity, the implementation removes the oldest entry (the first key returned by the Map iterator). Additionally, on cache hits, entries are deleted and re-inserted to mark them as most-recently-used.
How does the LRU cache prevent duplicate renders for concurrent requests?
The cache implementation tracks in-flight renders using a separate lookup mechanism (typically a Promise-based map) alongside the LRU cache. When a cache miss occurs, the system checks if a render is already underway for that key. If so, it awaits the existing promise rather than initiating a new render, ensuring that concurrent requests for the same uncached page share the same HTML generation process.
Why does Instatic use a global publish version instead of individual cache keys?
Instatic uses a global monotonic version (accessed via getPublishVersion()) rather than fine-grained cache keys to ensure atomic consistency across the entire site. When content changes, incrementing a single global version and clearing the cache guarantees that no stale fragments persist. This approach simplifies invalidation logic and eliminates the risk of serving pages composed of mismatched content versions, which could occur with selective key invalidation.
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 →