Introduction
A production incident at 2 AM isn't just a problem for your engineering team—it's a crisis for your customers who have no idea what's happening. Status pages: what every monitoring developer should know is that they're not optional luxuries; they're a critical bridge between your infrastructure and customer trust. When incidents strike, a well-designed status page communicates transparently, reduces support load, and keeps your brand reputation intact.
Over the past 70 days, we've built robust monitoring stacks with Prometheus and Grafana. Now we're taking the final step: making that visibility useful to the people who matter most. Status pages let customers see real-time component health, subscribe to incident notifications, and understand exactly when their service will be restored. Let's explore how to build them the right way.
Automate component status from probes
Your monitoring system already knows which services are up or down. The first mistake teams make is manually updating status pages—that's slow, error-prone, and defeats the purpose of having automated monitoring. Instead, we should derive component status directly from the same probes that feed Prometheus and alert your team.
When you've instrumented your systems with Prometheus exporters and configured alert rules, you have a source of truth for service health. A status page should consume that same data. The pattern looks like this: probes check component health on regular intervals, metrics flow into Prometheus, and a status page service queries those metrics (or alert state) to determine what to display.
Consider a typical e-commerce stack with API, database, payment gateway, and CDN. Each component has dedicated probes:
---
# Prometheus alert rules for status page
groups:
- name: component_health
interval: 30s
rules:
- alert: APIDown
expr: up{job="api"} == 0
for: 1m
annotations:
component: "API Server"
status: "degraded"
- alert: DatabaseDown
expr: up{job="postgres"} == 0
for: 2m
annotations:
component: "Primary Database"
status: "down"
- alert: PaymentGatewayDown
expr: probe_success{job="payment_check"} == 0
for: 5m
annotations:
component: "Payment Gateway"
status: "down"
- alert: CDNLatencyHigh
expr: histogram_quantile(0.95, cdn_request_duration_seconds) > 2
for: 10m
annotations:
component: "CDN"
status: "degraded"
Your status page backend queries Prometheus at regular intervals—typically every 30 to 60 seconds—and extracts component states:
---
# Example: Node.js service fetching component status
async function updateComponentStatus() {
const promClient = new PrometheusClient();
const components = [
{ name: "API", metric: 'up{job="api"}' },
{ name: "Database", metric: 'up{job="postgres"}' },
{ name: "Payment", metric: 'probe_success{job="payment_check"}' },
{ name: "CDN", metric: 'histogram_quantile(0.95, cdn_request_duration_seconds)' }
];
for (const comp of components) {
const result = await promClient.query(comp.metric);
const value = parseFloat(result[0]?.value?.[1] ?? 0);
let status = "operational";
if (comp.name === "CDN" && value > 2) {
status = "degraded";
} else if (value === 0) {
status = "down";
}
await db.updateComponent({
name: comp.name,
status: status,
lastChecked: new Date()
});
}
}
// Run every 60 seconds
setInterval(updateComponentStatus, 60000);
This approach keeps your status page accurate without manual intervention. Probes run continuously, metrics update in real time, and your status page reflects the true state of your infrastructure.
Post updates during incidents
Automated status alone isn't enough. When an incident occurs, customers need more than a red indicator—they need context. Your status page should support manual incident posts that include what's happening, what you're doing about it, and when you expect resolution.
A typical incident post workflow: on-call engineer identifies a critical issue, opens an incident in your incident management system (PagerDuty, Opsgenie), and simultaneously posts to the status page. The post should be posted in real time, not after the fix is complete.
Good incident posts have a structure:
- Initial notice: Acknowledge the issue within 2-3 minutes. "We're investigating reports of degraded API performance."
- Progress updates: Every 15-30 minutes during active incidents. "Root cause identified: database connection pool exhaustion. Scaling connections now."
- Resolution: When services are restored. "Incident resolved at 14:32 UTC. All systems operational."
- Postmortem link (optional): After stabilization. "We'll publish a full incident report by EOD Wednesday."
Here's a practical incident posting interface:
---
# POST /api/incidents
{
"title": "API Latency Degradation",
"components": ["API", "Database"],
"severity": "major",
"message": "Investigating elevated API response times. Current ETA for update: 15 minutes.",
"status": "investigating"
}
# The status page updates immediately:
# - Affected components shift to "degraded" or "down"
# - Incident timeline shown to visitors
# - Next update expected field displayed
# Follow-up: PATCH /api/incidents/{id}
{
"message": "Root cause found: connection pool saturation. Scaling in progress.",
"status": "identified"
}
# Final: PATCH /api/incidents/{id}
{
"message": "All systems restored and operating normally.",
"status": "resolved",
"resolvedAt": "2024-01-15T14:32:00Z"
}
The key is speed and transparency. Customers would rather see "investigating, unknown ETA" at minute one than silence until minute 30 when you have answers. That transparency builds trust, even during failure.
Subscribe options for customers
Customers shouldn't have to manually check your status page every 30 seconds during an incident. Provide subscription channels so they receive notifications when component status changes. Common options: email, SMS, webhook, and RSS.
Email is the baseline. When a component transitions from operational to degraded, send an immediate notification to all subscribers who've opted in. Keep emails brief and actionable: component name, new status, link to the status page, and ETA if known.
For critical infrastructure customers, offer SMS or Slack notifications for faster alert delivery. Webhooks allow enterprise customers to integrate status updates directly into their own dashboards or runbooks. RSS feeds serve customers who prefer passive subscriptions.
A subscription model looks like this:
---
# Subscription schema
{
"id": "sub_abc123",
"email": "ops@customer.com",
"channels": ["email", "webhook"],
"components": ["API", "Database"], # null = all components
"notifyOn": ["operational_to_degraded", "degraded_to_down", "resolved"],
"createdAt": "2024-01-10T09:15:00Z"
}
# Webhook configuration
{
"webhook_url": "https://customer.example.com/status-webhook",
"webhook_secret": "sk_test_...", # HMAC signing for verification
"retryPolicy": {
"maxRetries": 5,
"backoffSeconds": 60
}
}
When posting a status update, your backend finds all relevant subscriptions and dispatches notifications:
---
# Notification dispatch
async function notifySubscribers(componentId, oldStatus, newStatus) {
const subs = await db.findSubscriptions({
components: [componentId, null] # null = subscribed to all
});
for (const sub of subs) {
const event = { component: componentId, oldStatus, newStatus, timestamp: now() };
if (sub.channels.includes("email")) {
await sendEmail(sub.email, event);
}
if (sub.channels.includes("webhook")) {
await postWebhook(sub.webhook_url, event, sub.webhook_secret);
}
}
}
Self-service subscription management is critical. Customers must be able to add their email, choose which components matter to them, and unsubscribe. A simple web form with email confirmation prevents spam and ensures accuracy.
Keep it honest
The biggest mistake teams make is using the status page as a marketing tool rather than a truth-telling tool. If your system is struggling, say so. If you don't know the root cause yet, admit it. This honesty is counterintuitive but far more valuable than spin.
When a component is degraded—say, API p99 latency spiked but it's still serving requests—mark it degraded, not operational. When you're investigating but have no ETA, write "investigating—no ETA" instead of "ETA 5 minutes" and then missing that deadline five times.
Customers can tolerate incidents. They cannot tolerate feeling lied to. A status page that consistently misrepresents reality will erode trust far more than honest communication about what's broken and when it will be fixed.
Some practical rules for honesty:
- Update at least every 30 minutes during active incidents, even if the update is "still investigating."
- Use clear language: "down," "degraded," "operational"—not vague terms like "experiencing issues."
- List actual impact: "Affecting 15% of signup requests" is better than "API degradation."
- When you resolve early, celebrate it. When you miss an ETA, explain why and give a new one.
- Post incident summaries within 48 hours. Not months later when everyone forgot.
This transparency typically reduces support volume during incidents because customers see the status page is being kept current, reducing the need to email support asking "is this affecting me?"
Common mistakes
- Updating status page manually: Lag and inconsistency are inevitable. Automate probe data into your status page or maintain a API sync that queries Prometheus continuously.
- Creating component categories with unclear scope: Don't call something "Infrastructure." Be specific: "API Server," "Payment Processing," "Search Service." Customers won't know what "Infrastructure" means for their use case.
- Omitting historical incident data: Keep incident timelines visible for 90 days. They prove you've had outages and resolved them, and help customers understand your stability.
- Not testing subscription delivery: Send a test notification every week. Email servers go down, webhooks fail. If you haven't verified subscriptions work in two weeks, they probably don't.
- Resolving incidents without follow-up: Once resolved, post a brief postmortem link or "more info coming" message. Customers appreciate knowing you're learning from it.
- Ignoring timezone issues: Display all times in UTC and let customers' browsers convert to local time, or allow timezone selection. Ambiguous times create confusion during critical moments.
Key takeaways
- Automate component status from Prometheus metrics and alert rules—never rely on manual updates during incidents.
- Post incident updates frequently (at least every 30 minutes) with honest ETA, impact, and current status.
- Offer multiple subscription channels: email, webhook, SMS so customers stay informed without constant manual checking.
- Prioritize transparency over spin; customers trust teams that admit unknowns more than teams that manufacture false confidence.
- Keep historical incident data visible and follow up with postmortem summaries within 48 hours to close the loop.
- Test subscription delivery regularly; a broken notification system during an incident is worse than no subscriptions at all.






Comments
Loading comments…