ISR Monitoring — Monitoring Revalidation Health and Performance
In this tutorial, you will learn about ISR Monitoring. We cover key concepts, practical examples, and best practices to help you master this topic.
ISR monitoring tracks revalidation success rates, cache freshness, generation times, and error rates to ensure static content stays fresh and fast.
What You'll Learn
By the end of this tutorial, you'll understand how to monitor ISR health, track revalidation metrics, set up alerting for failures, and build dashboards to visualize ISR performance.
Why It Matters
ISR failures are silent — stale content continues serving without obvious errors. Without monitoring, you won't know when content is stuck at an old version. Monitoring ensures your ISR promises of fresh content are actually kept.
Real-World Use
A news site monitors 10,000 ISR pages. The dashboard shows revalidation success rate (99.8%), average generation time (320ms), and pages that haven't been revalidated in over 24 hours. Alerts fire when success rate drops below 95%.
Monitoring Architecture
graph TD
A[ISR Events] --> B[Revalidation attempts]
A --> C[Cache generation]
A --> D[Cache serves]
B --> E[Success counter]
B --> F[Failure counter]
C --> G[Generation time]
C --> H[Page size]
D --> I[Cache hit/miss]
D --> J[Cache age]
E --> K[Metrics Dashboard
Grafana / Datadog]
F --> K
G --> K
H --> K
I --> K
J --> K
K --> L[Alerts
Slack / PagerDuty]
style B fill:#4a90d9,color:#fff
style C fill:#e67e22,color:#fff
style D fill:#27ae60,color:#fff
style K fill:#f39c12,color:#fff
Revalidation Logging
// lib/isr-monitor.js — ISR event logging
class ISRMonitor {
constructor() {
this.metrics = {
revalidations: {
total: 0,
succeeded: 0,
failed: 0,
totalDuration: 0
},
cache: {
hits: 0,
misses: 0,
staleServes: 0
},
pages: new Map() // path -> metrics
};
this.logBuffer = [];
this.flushInterval = setInterval(() => this.flush(), 5000);
}
logRevalidation(path, success, duration, error = null) {
this.metrics.revalidations.total++;
this.metrics.revalidations.totalDuration += duration;
if (success) {
this.metrics.revalidations.succeeded++;
} else {
this.metrics.revalidations.failed++;
}
// Per-page metrics
if (!this.metrics.pages.has(path)) {
this.metrics.pages.set(path, {
revalidations: 0,
failures: 0,
lastRevalidation: null,
lastDuration: 0
});
}
const pageMetrics = this.metrics.pages.get(path);
pageMetrics.revalidations++;
pageMetrics.lastRevalidation = Date.now();
pageMetrics.lastDuration = duration;
if (!success) {
pageMetrics.failures++;
}
// Structured log entry
this.logBuffer.push({
timestamp: new Date().toISOString(),
event: 'revalidation',
path,
success,
duration,
error: error?.message || null,
served: success ? 'fresh' : 'stale-kept'
});
// Alert if consecutive failures
if (pageMetrics.failures >= 3) {
this.triggerAlert({
type: 'consecutive_failures',
path,
failures: pageMetrics.failures,
lastError: error?.message
});
}
}
logCacheServe(path, source) {
if (source === 'cache') {
this.metrics.cache.hits++;
} else if (source === 'stale') {
this.metrics.cache.staleServes++;
} else {
this.metrics.cache.misses++;
}
}
getSummary() {
const { revalidations, cache } = this.metrics;
const successRate = revalidations.total > 0
? (revalidations.succeeded / revalidations.total * 100).toFixed(1)
: 100;
return {
revalidations: {
...revalidations,
successRate: `${successRate}%`,
avgDuration: revalidations.total > 0
? `${(revalidations.totalDuration / revalidations.total).toFixed(0)}ms`
: 'N/A'
},
cache: {
...cache,
hitRate: `${((cache.hits / (cache.hits + cache.misses + cache.staleServes)) * 100).toFixed(1)}%`
},
stalePages: this.getStalePages(3600)
};
}
getStalePages(maxAgeSeconds) {
const now = Date.now();
const stale = [];
for (const [path, metrics] of this.metrics.pages) {
if (metrics.lastRevalidation &&
(now - metrics.lastRevalidation) > maxAgeSeconds * 1000) {
stale.push({
path,
age: Math.floor((now - metrics.lastRevalidation) / 1000),
lastRevalidation: new Date(metrics.lastRevalidation).toISOString()
});
}
}
return stale.sort((a, b) => b.age - a.age);
}
triggerAlert(alert) {
console.error('[ISR ALERT]', JSON.stringify(alert, null, 2));
// In production, send to Slack/PagerDuty/Sentry
if (process.env.ALERT_WEBHOOK_URL) {
fetch(process.env.ALERT_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `ISR Alert: ${alert.type}\nPath: ${alert.path}\nFailures: ${alert.failures}\nError: ${alert.lastError}`,
severity: 'warning'
})
}).catch(() => {});
}
}
flush() {
if (this.logBuffer.length > 0) {
// Send logs to your logging service
if (process.env.LOG_ENDPOINT) {
fetch(process.env.LOG_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.logBuffer)
}).catch(() => {});
}
this.logBuffer = [];
}
}
}
export const isrMonitor = new ISRMonitor();
Page Generation Timestamp
// components/GenerationTimestamp.jsx — Display cache freshness
export default function GenerationTimestamp({ generatedAt, revalidate }) {
const [age, setAge] = useState(0);
const [isStale, setIsStale] = useState(false);
useEffect(() => {
const updateAge = () => {
const seconds = Math.floor((Date.now() - generatedAt) / 1000);
setAge(seconds);
setIsStale(seconds > revalidate);
};
updateAge();
const interval = setInterval(updateAge, 1000);
return () => clearInterval(interval);
}, [generatedAt, revalidate]);
const formatAge = (seconds) => {
if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
};
return (
<div className={`generation-timestamp ${isStale ? 'stale' : 'fresh'}`}>
<span className="indicator" />
<span className="label">
{isStale ? 'Refreshing...' : 'Fresh'}
</span>
<span className="age">{formatAge(age)} ago</span>
<span className="revalidate">
(revalidates every {revalidate}s)
</span>
</div>
);
}
Monitoring Dashboard API
// pages/api/isr-status.js — ISR monitoring endpoint
import { isrMonitor } from '../../lib/isr-monitor';
export default async function handler(req, res) {
// Require authentication for monitoring
const auth = req.headers.authorization;
if (auth !== `Bearer ${process.env.MONITORING_API_KEY}`) {
return res.status(401).json({ error: 'Unauthorized' });
}
const summary = isrMonitor.getSummary();
// Add system health
const health = {
status: summary.revalidations.successRate >= 95 ? 'healthy' : 'degraded',
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: Date.now()
};
res.json({
...summary,
health,
_meta: {
generatedAt: Date.now(),
version: '1.0.0'
}
});
}
// Health check endpoint for uptime monitoring
// pages/api/health.js
export default function handler(req, res) {
res.json({
status: 'ok',
isrEnabled: true,
timestamp: Date.now()
});
}
Integration with External Monitoring
// lib/external-monitoring.js — Connect to APM tools
class ExternalMonitoring {
constructor() {
this.apmAvailable = !!process.env.APM_SERVICE_URL;
}
// Sentry integration
captureRevalidationError(error, context) {
if (typeof Sentry !== 'undefined') {
Sentry.captureException(error, {
tags: { service: 'isr' },
extra: context
});
}
}
// Datadog integration
sendMetric(name, value, tags = {}) {
if (process.env.DATADOG_API_KEY) {
// Send to Datadog API
fetch('https://api.datadoghq.com/api/v1/series', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'DD-API-KEY': process.env.DATADOG_API_KEY
},
body: JSON.stringify({
series: [{
metric: `isr.${name}`,
points: [[Date.now(), value]],
tags: Object.entries(tags).map(([k, v]) => `${k}:${v}`)
}]
})
}).catch(() => {});
}
}
// Slack alert
async sendAlert(message, severity = 'warning') {
if (process.env.SLACK_WEBHOOK_URL) {
const colors = { info: '#3498db', warning: '#f39c12', error: '#e74c3c' };
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
color: colors[severity],
text: message,
footer: 'ISR Monitor',
ts: Math.floor(Date.now() / 1000)
}]
})
});
}
}
}
export const externalMonitor = new ExternalMonitoring();
Common Mistakes
- Not monitoring ISR at all. Silent failures mean stale content indefinitely. Set up basic monitoring from day one.
- Only tracking revalidation rate without success rate. A high revalidation rate with 50% failures means half your content is stale. Track both.
- Ignoring generation time trends. Slowly increasing generation times indicate database or API performance degradation. Set trend alerts.
- Not alerting on stale pages. Pages that haven't been revalidated in 24+ hours may have broken data fetching. Alert on maximum age thresholds.
- Forgetting to include generation timestamps in the page. Users (and search engines) benefit from knowing when the page was generated. Display it.
Practice Questions
- What metrics should you track for ISR health monitoring?
- How do you detect a silent ISR failure?
- What alerting thresholds should you set for ISR?
- How do you integrate ISR monitoring with external APM tools?
- Why is generation timestamp display important for both users and developers?
Challenge: Build a comprehensive ISR monitoring system: implement revalidation logging with success/failure tracking, create a monitoring dashboard API endpoint, set up Slack alerts for consecutive failures, add page-level generation timestamps, and integrate with an external monitoring service.
FAQ
Mini Project
Create a full ISR monitoring setup: implement in-memory metric tracking, expose a status dashboard API, add Slack alerts for failures, display generation timestamps on pages, set up a cron job that checks stale page thresholds, and benchmark monitoring overhead on revalidation time.
What's Next
You've mastered ISR monitoring. Now build your final project: complete the ISR Mini Project to apply everything you've learned about incremental static regeneration.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro