Skip to content

ISR Monitoring — Monitoring Revalidation Health and Performance

DodaTech Updated 2026-06-28 7 min read

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

  1. Not monitoring ISR at all. Silent failures mean stale content indefinitely. Set up basic monitoring from day one.
  2. Only tracking revalidation rate without success rate. A high revalidation rate with 50% failures means half your content is stale. Track both.
  3. Ignoring generation time trends. Slowly increasing generation times indicate database or API performance degradation. Set trend alerts.
  4. Not alerting on stale pages. Pages that haven't been revalidated in 24+ hours may have broken data fetching. Alert on maximum age thresholds.
  5. Forgetting to include generation timestamps in the page. Users (and search engines) benefit from knowing when the page was generated. Display it.

Practice Questions

  1. What metrics should you track for ISR health monitoring?
  2. How do you detect a silent ISR failure?
  3. What alerting thresholds should you set for ISR?
  4. How do you integrate ISR monitoring with external APM tools?
  5. 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

What is a healthy ISR success rate?

99%+ is healthy. Below 95% indicates problems with data fetching, API availability, or server function timeouts.

How do I monitor ISR on Vercel?

Vercel provides ISR metrics in the dashboard under Analytics > ISR. You can also export logs to external services.

Can I monitor ISR without external services?

Yes. Build in-memory metrics, expose a status API endpoint, and add health check monitoring from your uptime service.

How often should I check cache freshness?

Every request. Display the generation timestamp on the page. Users can see how fresh the content is and you can track stale page counts.

What is the most common ISR failure?

External API or database timeouts during revalidation. The data source is unavailable, the revalidation fails, and the stale page continues serving.

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