ISR (Incremental Static Regeneration)
ISR enables static pages to be updated after deployment without rebuilding the entire site.
Overview
ISR combines the benefits of static generation with dynamic content freshness:
Static at Build - Pages are pre-rendered at build time
Cached Serving - Requests served from cache with minimal latency
Background Revalidation - Stale content triggers async regeneration
No Rebuild Required - Content updates without full site rebuild
How It Works
Request โ Cache Check
โ
โโโโโโโดโโโโโโ
โ โ
Fresh? Stale? Miss?
โ โ โ
Return Return + Generate
cached schedule + cache
revalidate
Cache States
| State | Condition | Behavior |
|---|---|---|
| Fresh | now < generated_at + TTL | Return cached immediately |
| Stale | now >= generated_at + TTL | Return cached + revalidate in background |
| Miss | No cache entry | Generate, cache, return |
Configuration
Frontmatter
Enable ISR for a page by adding revalidate to frontmatter:
---
title: My Page
revalidate: 60
---
# Content here
The revalidate value is in seconds.
TTL Guidelines
| Content Type | Recommended TTL | Example |
|---|---|---|
| Hot (high traffic) | 300-600s | Homepage, popular posts |
| Warm (moderate) | 60-300s | Tutorials, API docs |
| Cold (low traffic) | 30-60s | Archive, old posts |
| Real-time | 0 | Always regenerate |
Build Output
When pages have revalidate set, Sol SSG generates an ISR manifest:
dist/
โโโ _luna/
โโโ isr.json
Manifest Format
{
"version": 1,
"pages": {
"/blog/": {
"revalidate": 300,
"renderer": "markdown",
"source": "blog/index.md"
},
"/blog/post-1/": {
"revalidate": 120,
"renderer": "markdown",
"source": "blog/post-1.md"
}
}
}
Runtime Behavior
Sol Server Integration
Sol automatically loads the ISR manifest and handles caching:
// Initialize ISR handler
let handler = init_isr(dist_dir)
// Handle request
let (html, needs_revalidation) = handler.handle(path)
if needs_revalidation {
schedule_revalidation(path)
}
Cache Key Format
Cache keys include path and query parameters:
isr:/blog/ # Simple path
isr:/search/?q=luna&sort=date # With sorted query params
Query parameters are sorted alphabetically for consistent cache keys.
Performance
ISR is optimized for high throughput:
| Operation | Throughput | Latency |
|---|---|---|
| Cache read | ~6.7M ops/sec | 0.15ฮผs |
| Cache write | ~2.7M ops/sec | 0.36ฮผs |
| Status check | ~4.4M ops/sec | 0.23ฮผs |
| Full handle | ~2.4M req/sec | 0.41ฮผs |
The cache overhead is negligible compared to network I/O.
Example: Blog with Tiered TTLs
docs/
โโโ blog/
โ โโโ index.md # revalidate: 300 (hot)
โ โโโ post-1.md # revalidate: 120 (warm)
โ โโโ post-2.md # revalidate: 120 (warm)
โ โโโ archive/
โ โโโ index.md # revalidate: 60 (cold)
โ โโโ old-post.md # revalidate: 60 (cold)
Traffic Pattern Simulation
With 50,000 pages following the 80/20 rule:
| Tier | Pages | TTL | Traffic Share |
|---|---|---|---|
| Hot | 100 | 300s | 80% |
| Warm | 900 | 120s | 15% |
| Cold | 49,000 | 60s | 5% |
Hot pages stay fresh due to frequent access. Cold pages serve stale content briefly then revalidate.
Stale-While-Revalidate Pattern
ISR implements the SWR pattern:
User A requests stale page โ Gets stale content immediately (no wait)
Background regeneration starts
User B requests same page โ Gets fresh content
This ensures users never wait for regeneration.
Deployment Considerations
Cloudflare Workers
ISR works with Cloudflare's edge caching:
{
"deploy": "cloudflare"
}
The ISR handler integrates with waitUntil for background revalidation.
Memory Cache
For development and single-instance deployments:
let cache = MemoryCache::new()
Note: Memory cache is lost on restart. For production, use distributed cache.
API Reference
Frontmatter Options
| Option | Type | Default | Description |
|---|---|---|---|
revalidate | Int | None | TTL in seconds (0 = always stale) |
ISRHandler
// Create handler
fn ISRHandler::new(cache, manifest, dist_dir) -> ISRHandler
// Handle request - returns (html?, needs_revalidation)
fn handle(path: String) -> (String?, Bool)
// Update cache after revalidation
fn update_cache(path: String, html: String) -> Unit
CacheEntry
struct CacheEntry {
html: String // Cached HTML content
generated_at: Int64 // Unix timestamp (ms)
revalidate: Int // TTL in seconds
}
CacheStatus
enum CacheStatus {
Fresh // Within TTL
Stale // Past TTL but content available
Miss // No cached content
}
Best Practices
Use appropriate TTLs - Match TTL to content update frequency
Monitor cache hit rates - Adjust TTLs based on actual usage
Handle errors gracefully - Serve stale content on regeneration failure
Consider traffic patterns - Hot pages benefit most from longer TTLs
Test with realistic data - Simulate production traffic patterns
Limitations
Memory cache doesn't persist across restarts
Revalidation requires server-side execution
Query parameter variations create separate cache entries
Path matching is case-sensitive and exact (trailing slash matters)