March 12, 2026 (3mo ago)
Written by Temps Team
Last updated March 12, 2026 (3mo ago)
Adding a status page to your app takes less than an afternoon using open-source tools — and it immediately cuts support ticket volume by 30-40% during outages. Statuspage.io charges $29/month for a single page; this guide shows you the free paths: build it yourself in ~80 lines of code, use Gatus or Upptime, or deploy Temps and get a status page included automatically at no extra cost.
TL;DR: A status page reduces support tickets by 30-40% during outages. You can build one with health check endpoints, a polling aggregator, and a static HTML page — roughly 80 lines of code. Or use an open-source tool like Gatus or Upptime. Temps includes a status page per project with automatic health checks every 60 seconds, no extra setup needed.
The fastest path is four steps: expose a /health endpoint, run a polling script every 60 seconds, write results to a JSON file, and serve a static HTML page from a separate host. The whole thing fits in about 80 lines of code across three files.
If you want something pre-built, Gatus and Upptime are the two open-source options worth reaching for. Both are actively maintained, free to self-host, and require no custom code.
If you want a status page that is automatically created for every deployment — with zero configuration — Temps includes one as part of its deployment platform. Deploy an app, and the status page exists.
Teams with public status pages resolve incidents 20% faster on average, according to PagerDuty's State of Digital Operations report. A status page isn't just about transparency — it directly reduces the operational cost of every outage you'll ever have.
When your app goes down, users panic. They can't tell if it's their network, their browser, or your server. So they file tickets. Every single one of those tickets costs you time — reading, triaging, responding with "we're aware of the issue."
A status page short-circuits that entire loop. Users check the page, see the incident banner, and wait. Your support queue stays manageable. Your on-call engineer stays focused on actually fixing the problem instead of answering emails.
If you sell to businesses, you'll hit SLA requirements sooner than you think. SOC 2 audits ask about incident communication procedures. Enterprise procurement teams check whether you have a public status page before signing contracts.
According to Gartner, 78% of enterprise IT buyers consider vendor transparency during outages a key evaluation criterion. A status page is often the simplest way to check that box.
Have you ever received a bug report that was actually just a service outage? It happens constantly. Without a status page, users can't distinguish between "the app is broken for everyone" and "something is wrong on my end."
A visible status indicator saves both sides time. Users self-triage. Your engineering team gets fewer false bug reports. Everyone wins.
A well-designed status page covers five elements. According to Pingdom's downtime survey, 92% of users expect real-time status updates during outages. Meeting that expectation requires more than a green checkmark.
Don't show a single "up" or "down" indicator for your entire app. Break it into components: API, web app, database, authentication, CDN, background jobs. Users need to know which parts are affected.
Three states work well: Operational, Degraded Performance, and Major Outage. Some teams add a fourth — Partial Outage — for situations where only some regions or user segments are impacted.
Every incident needs a timeline. When did it start? What's being done? When was it resolved? Post updates every 20-30 minutes during active incidents, even if the update is "still investigating."
The worst thing you can do is post "we're investigating" and go silent for two hours. Users assume you've forgotten about them. Frequent updates — even without new information — signal that the team is actively working.
Show a rolling 90-day uptime percentage per component. This gives users context. Is this a rare blip on a 99.99% track record, or is it the third outage this month on a service struggling to hit 99.5%?
A simple bar chart with daily uptime works well. Green bars for clean days, yellow for degraded, red for outages. GitHub's status page does this effectively.
Proactive communication matters as much as reactive. Post maintenance windows at least 48 hours in advance. Include the expected duration, which components are affected, and whether users will experience downtime.
A latency graph for the past 24-72 hours shows users whether performance has been degrading. If your API response time crept from 100ms to 800ms over four hours before the outage, that context helps users understand what happened.
A status page system has five parts: health check endpoints, an aggregator, status computation, a public page, and a notification system. According to the Stack Overflow Developer Survey, 62% of professional developers manage some form of infrastructure monitoring. Yet many still cobble together ad-hoc solutions instead of building proper health check infrastructure.
Every service in your stack needs a /health endpoint. This isn't a simple return 200 — a good health check verifies that the service can actually do its job.
For an API server, that means checking database connectivity, cache availability, and external service reachability. For a background worker, it means verifying the job queue is connected and processing. A health endpoint that always returns 200 is worse than no health endpoint — it gives you false confidence.
// Good health check response
{
"status": "healthy",
"checks": {
"database": { "status": "up", "latency_ms": 3 },
"redis": { "status": "up", "latency_ms": 1 },
"storage": { "status": "up", "latency_ms": 12 }
},
"version": "2.4.1",
"uptime_seconds": 847293
}
The aggregator is a service (or cron job) that polls each health endpoint at regular intervals — typically every 30-60 seconds. It records the response status, latency, and any error details.
Keep the polling interval consistent. Irregular checks create noisy data. And always poll from outside your infrastructure — checking health from the same server that runs the service doesn't tell you much about actual user reachability.
Raw health check data needs processing before it becomes useful status information. A single failed check shouldn't flip a component to "Major Outage" — network blips happen. Most systems use a threshold: repeated consecutive failures trigger a status change, and recovery requires multiple consecutive successes to prevent flapping.
The public-facing status page should be hosted separately from your main application. If your app is down, your status page needs to still be accessible. Many teams host the status page on a separate subdomain (status.yourapp.com) with a different hosting provider or CDN.
Subscribers should be able to opt in for email, webhook, or Slack notifications. When a component status changes, the system fires notifications to all subscribers for that component. Keep it simple — status change events in, notifications out.
Building a minimal status page takes roughly 80 lines of code and an afternoon. The Node.js ecosystem alone has over 2,000 packages related to health checks on npm. But you don't need any of them for a basic setup.
We've found that the simplest status page implementations — a polling script, a JSON file, and a static HTML page — tend to be the most reliable. Every dependency you add is another thing that can break during an outage, which is exactly when you need the status page to work.
Start with a /health route in your application. Here's a Node.js/Express example that checks database and Redis connectivity:
app.get('/health', async (req, res) => {
const checks = {};
// Check database
try {
const start = Date.now();
await db.query('SELECT 1');
checks.database = { status: 'up', latency_ms: Date.now() - start };
} catch (err) {
checks.database = { status: 'down', error: err.message };
}
// Check Redis
try {
const start = Date.now();
await redis.ping();
checks.redis = { status: 'up', latency_ms: Date.now() - start };
} catch (err) {
checks.redis = { status: 'down', error: err.message };
}
const allUp = Object.values(checks).every(c => c.status === 'up');
res.status(allUp ? 200 : 503).json({
status: allUp ? 'healthy' : 'unhealthy',
checks,
timestamp: new Date().toISOString()
});
});
A simple polling script runs on a schedule, checks each endpoint, and writes results to a JSON file or database:
const SERVICES = [
{ name: 'API', url: 'https://api.yourapp.com/health' },
{ name: 'Web App', url: 'https://yourapp.com/health' },
{ name: 'Worker', url: 'https://worker.yourapp.com/health' },
];
async function pollServices() {
const results = [];
for (const service of SERVICES) {
try {
const start = Date.now();
const res = await fetch(service.url, { signal: AbortSignal.timeout(5000) });
results.push({
name: service.name,
status: res.ok ? 'operational' : 'degraded',
latency_ms: Date.now() - start,
checked_at: new Date().toISOString()
});
} catch (err) {
results.push({
name: service.name,
status: 'down',
error: err.message,
checked_at: new Date().toISOString()
});
}
}
// Write to JSON file (or insert into database)
await fs.writeFile('./status.json', JSON.stringify(results, null, 2));
}
// Run every 60 seconds
setInterval(pollServices, 60_000);
pollServices();
The simplest approach is a static HTML file that fetches the JSON data and renders it. Host this on a separate server or CDN — it needs to work even when your main app is down.
<!DOCTYPE html>
<html>
<head>
<title>Status - YourApp</title>
<style>
.status-item { padding: 12px; margin: 8px 0; border-radius: 6px; }
.operational { background: #d4edda; color: #155724; }
.degraded { background: #fff3cd; color: #856404; }
.down { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>System Status</h1>
<div id="services"></div>
<script>
fetch('/status.json')
.then(r => r.json())
.then(services => {
const el = document.getElementById('services');
el.innerHTML = services.map(s =>
`<div class="status-item ${s.status}">
<strong>${s.name}</strong>: ${s.status}
${s.latency_ms ? `(${s.latency_ms}ms)` : ''}
</div>`
).join('');
});
</script>
</body>
</html>
That's roughly 80 lines across three files. It won't win any design awards, but it works. Run the aggregator as a systemd service or cron job, deploy the HTML to a CDN, and you've got a functional status page.
The open-source ecosystem offers several mature alternatives to building from scratch. GitHub Topics lists over 300 repositories tagged "status-page". Here are the ones actually worth using.
Upptime runs entirely on GitHub Actions. No server required. It stores uptime data in the repository itself, uses GitHub Issues for incidents, and deploys a static status page via GitHub Pages.
Why would you want this? Zero infrastructure cost. GitHub Actions gives you the compute, GitHub Pages gives you the hosting, and the repository itself becomes your database. The tradeoff is that you're limited by GitHub Actions quotas and can't customize beyond what the templating system offers.
Gatus is a single Go binary configured entirely through YAML. Define your endpoints, set alerting conditions, and run it. It supports HTTP, TCP, DNS, ICMP, and even SSH health checks out of the box.
What sets Gatus apart is its condition language. You can write checks like [STATUS] == 200 && [BODY].status == 'healthy' && [RESPONSE_TIME] < 500. That's more expressive than most monitoring tools at any price point.
Statping-ng is the successor to Statping (which, like Cachet, went unmaintained for a while). It includes a web dashboard, multiple notification integrations (Slack, Discord, email, Telegram), and a REST API.
It's heavier than Gatus — it needs a database (SQLite, PostgreSQL, or MySQL) — but gives you more out of the box. If you want a polished web interface without building one, Statping-ng is the closest to a drop-in Statuspage.io replacement.
Cstate generates a static status page using Hugo. You define incidents and component status in Markdown files, and Hugo builds a fast, CDN-friendly static site. It's the lightest option — no backend, no database, no running service.
The downside: incident updates are manual. You create a Markdown file for each incident, which is fine for small teams but doesn't scale if you want automated status changes based on health checks.
Most teams start with a simple tool like Upptime or Cstate and graduate to something like Gatus once they hit 10+ services. The mistake is starting with a heavy tool before you know what you actually need to monitor. Pick the lightest option that covers your current requirements.
| Feature | Temps | Statuspage.io | Gatus |
|---|---|---|---|
| Price | Free (self-host) or ~$6/mo cloud | $29+/mo | Free (self-host) |
| License | Apache 2.0 | Proprietary SaaS | Apache 2.0 |
| Setup | Zero config — auto-created per deployment | Manual configuration | YAML config file |
| Check interval | Every 60 seconds | Every 1-5 minutes | Configurable (30s+ typical) |
| Retry logic | Up to 3 retries with exponential backoff | N/A | Configurable retries |
| Incident tracking | Automatic + manual | Manual | No built-in incidents |
| Deployment platform | Built-in (git push) | None | None |
| Analytics, error tracking | Built-in | None | None |
| Separate hosting for status page | Yes (Temps infrastructure) | Yes (Atlassian CDN) | Requires separate deploy |
| Tool | Language | Database | Auto-Detection | UI | Setup Time |
|---|---|---|---|---|---|
| Upptime | TypeScript | GitHub repo | Yes (Actions) | Static (Pages) | ~15 min |
| Gatus | Go | SQLite/Postgres | Yes (polling) | Built-in web | ~10 min |
| Statping-ng | Go | SQLite/Postgres/MySQL | Yes (polling) | Full dashboard | ~30 min |
| Cstate | Hugo | None (Markdown) | No (manual) | Static site | ~20 min |
| DIY | Any | Any | Custom | Custom | ~2 hours |
Temps includes a built-in status page for every project — no extra services, no configuration files, no separate hosting. According to Datadog's State of Monitoring report, the average organization uses 7.4 monitoring tools. Temps collapses the status page into your existing deployment platform so you don't add yet another tool to that number.
When you create a new environment in Temps, a monitor is automatically created for it. No configuration required. The StatusPagePlugin listens for EnvironmentCreated events on the job queue and calls ensure_monitor_for_environment — so by the time your first deployment completes, monitoring is already running.
Monitors are automatically removed when environments are deleted. There's no manual cleanup.
The HealthCheckService scheduler runs all active monitors in parallel (up to 10 concurrent checks) every 60 seconds. Each check uses the public-facing URL of your deployment — not the internal container network — so the check reflects what real users experience.
If your .temps.yaml config specifies a health_check_path, Temps uses that path on successful deployments. Otherwise it falls back to /health for monitors configured as type health, or the root URL for web monitors.
Failed checks retry up to 3 times with exponential backoff (100ms → 200ms → 400ms) before recording a failure. Timeout-only failures retry; connection-refused failures record immediately (retrying on a crashed container adds noise without any chance of success).
Temps maps HTTP response codes to four status levels:
Temps creates incident records automatically when health checks detect a problem. Each incident logs the start time, affected components, status transitions, and resolution time. You can also create manual incidents for planned maintenance or partial outages that automated checks don't catch.
Incidents are surfaced on the public status page immediately. Users see what happened, when it was detected, and the current resolution status.
Every project gets a status page that shows component health, uptime percentage, active incidents, and recent check history. The status page runs on Temps infrastructure — independent of your application containers — so it stays available even when your app is down. That's the key requirement most DIY solutions get wrong: hosting your status page on the same server as your app defeats the purpose.
To install Temps:
curl -fsSL https://temps.sh/install.sh | bash
Temps is free to self-host (Apache 2.0). Temps Cloud runs on Hetzner at ~$6/month with no per-seat fees.
Every 30-60 seconds is the sweet spot for most applications. More frequent checks (every 5-10 seconds) generate excessive load and noisy data without meaningfully faster detection. The Uptime Institute found that 76% of outages are detected within 2 minutes with 30-second polling intervals. Temps checks every 60 seconds, which strikes the balance between responsiveness and load. If you need sub-minute detection, you're looking at push-based health checks instead of polling.
Yes, always. If your application server goes down and your status page lives on the same server, users see nothing — which is worse than showing a "Major Outage" banner. Host your status page on a separate provider, CDN, or static hosting service. GitHub Pages, Cloudflare Pages, or a separate VPS in a different region all work. Temps handles this automatically: the status page runs on Temps infrastructure, separate from your application containers.
Uptime monitoring tells you when something breaks. A status page tells your users. They serve different audiences. Your monitoring tool alerts your on-call engineer at 3am. Your status page prevents 200 support tickets from landing in your inbox while that engineer is fixing the problem. You need both.
A static HTML page that shows component status and updates manually via git commits. That's it. Cstate does exactly this with Hugo. You don't need automated health checks, subscriber notifications, or historical charts on day one. Start with manual updates, add automation when the manual process becomes painful. Most teams hit that point around 5-10 incidents per quarter.
Temps distinguishes between connection failures and HTTP errors. A connection refused error records a failure immediately — there's no point retrying a crashed container. A timeout retries up to 3 times. HTTP 5xx errors retry up to 3 times with exponential backoff starting at 100ms. This avoids false alarms from transient network blips while still catching real outages quickly.
A status page is one of those things that seems optional until your first major outage. Then it becomes the difference between "we handled that well" and "why didn't anyone tell us the app was down?"
You've got three paths. Build it yourself with the code examples above — it's an afternoon of work and you'll understand exactly how it fits together. Pick an open-source tool like Gatus or Upptime if you want something battle-tested without the maintenance burden. Or use a platform that includes status pages alongside your deployment pipeline, so monitoring isn't another tool in your stack.
Temps gives you automatic status pages for every environment, health checks running every 60 seconds from a separate infrastructure layer, and incident tracking built in — all as part of a deployment platform that replaces Vercel, Sentry, PostHog, and Pingdom with a single Rust binary.
curl -fsSL https://temps.sh/install.sh | bash