Status Pages and SLA Reporting: Independent Hosting, Real Probes, and Gap-Free Credit Tables

Host the status page outside your failure domain, probe what customers actually do, and measure availability as the contract defines it. A lab finds a gap in a common credit table.

Enterprise customers care about two availability questions. During an incident: is it you or us, and when will it be fixed? After the month: did you meet what you promised, and what do we get if you did not? A status page answers the first; availability measurement and the service level agreement (SLA) answer the second.

Both are easy to get subtly wrong. A status page hosted next to the application goes down with it. A credit table written as "99.0% to 99.89%" and "99.9% to 100%" leaves availability values that match no row. This guide covers hosting, detection, measurement, and credit calculation, and a short lab computes downtime budgets by month length and demonstrates the gap in that common table.

Objectives, agreements, and budgets

  • A service level objective (SLO) is an internal target, such as 99.9% of requests succeeding over 30 days.
  • A service level agreement (SLA) is a contractual promise with consequences, usually service credits. It is normally looser than the SLO so you have room to react before you owe anything.
  • The error budget is the allowed unreliability: 100% minus the objective, over the measurement window.

Google's SRE book chapter on service level objectives explains why to choose objectives from what users need rather than from what you happen to achieve, and the SRE workbook's alerting on SLOs shows how to alert on budget burn rate.

Lab: budgets by month length, and a gap in a credit table

The program prints the downtime allowed by common objectives for 28-, 29-, 30-, and 31-day months and a 365-day year. It then checks five measured downtimes against a credit table written with the bands "99.0-99.89%" and "99.9-100%". Run it with go run .; it uses only the standard library.

// Lab: availability budgets by month length, and a gap in a common credit table.
package main

import (
	"fmt"
	"time"
)

func dur(seconds float64) string {
	d := time.Duration(seconds * float64(time.Second)).Round(time.Second)
	return d.String()
}

func main() {
	targets := []float64{99.0, 99.5, 99.9, 99.95, 99.99}
	fmt.Printf("%-8s", "target")
	for _, days := range []int{28, 29, 30, 31} {
		fmt.Printf("  %9s", fmt.Sprintf("%d days", days))
	}
	fmt.Printf("  %12s\n", "365-day year")
	for _, t := range targets {
		fmt.Printf("%-8s", fmt.Sprintf("%.2f%%", t))
		for _, days := range []int{28, 29, 30, 31} {
			fmt.Printf("  %9s", dur(float64(days*86400)*(100-t)/100))
		}
		fmt.Printf("  %12s\n", dur(365*86400*(100-t)/100))
	}

	// A table written as "99.0% - 99.89%" and "99.9% - 100%" leaves values in between.
	fmt.Println("\nMeasured availability that falls between '99.0-99.89%' and '99.9-100%' bands:")
	for _, down := range []float64{2592, 2600, 2700, 2851, 2852} { // seconds of downtime in a 30-day month
		a := 100 * (1 - down/(30*86400))
		band := "no band matches"
		switch {
		case a >= 99.9:
			band = "99.9-100%"
		case a >= 99.0 && a <= 99.89:
			band = "99.0-99.89%"
		}
		fmt.Printf("  %4.0f s down -> %.4f%% -> %s\n", down, a, band)
	}
}

Output recorded on November 3, 2026 (Go 1.26.5, windows/amd64):

target      28 days    29 days    30 days    31 days  365-day year
99.00%     6h43m12s   6h57m36s    7h12m0s   7h26m24s      87h36m0s
99.50%     3h21m36s   3h28m48s    3h36m0s   3h43m12s      43h48m0s
99.90%       40m19s     41m46s     43m12s     44m38s      8h45m36s
99.95%       20m10s     20m53s     21m36s     22m19s      4h22m48s
99.99%         4m2s      4m11s      4m19s      4m28s        52m34s

Measured availability that falls between '99.0-99.89%' and '99.9-100%' bands:
  2592 s down -> 99.9000% -> 99.9-100%
  2600 s down -> 99.8997% -> no band matches
  2700 s down -> 99.8958% -> no band matches
  2851 s down -> 99.8900% -> no band matches
  2852 s down -> 99.8900% -> 99.0-99.89%

What the output shows

Month length changes the budget. At 99.9%, February allows 40 minutes 19 seconds of downtime and a 31-day month allows 44 minutes 38 seconds. If your SLA says "99.9% per calendar month", state whether the budget is computed from that month's actual length. Most agreements do, and your reporting should too.

Bands written with two decimals leave holes. Between 2,592 and 2,851 seconds of downtime in a 30-day month, availability is below 99.9% but above 99.89%, so it matches neither row. A customer with 99.895% availability would reasonably ask which credit applies, and the table cannot say. Write bands as half-open ranges instead: "less than 99.9% but at least 99.0%". Amazon's EC2 SLA, for example, phrases its credit tiers this way.

Host the status page outside your failure domain

A status page exists for the moments your service is failing, so it must not depend on the same infrastructure. Common choices:

  • a hosted status page provider;
  • static files on a separate provider or account from your application, updated through an API that does not depend on your application.

Check the dependencies that people forget: the DNS provider, the identity provider your staff use to log in to the status tool, and the incident chat tool. Practice publishing an update during a game day with the main application turned off. Keep a documented manual route, such as a pre-authorized account on the status provider, for when automation is down too.

Detect what customers experience

Internal health checks tell you whether a process is running. They do not tell you whether a customer can sign in, load data, and save a change. Add synthetic probes that run a short real transaction from outside your network, for example with Amazon CloudWatch Synthetics or a similar service:

  1. Connect over TLS to the public hostname.
  2. Authenticate with a dedicated test account.
  3. Read a known record.
  4. Write a small record to a test tenant, then read it back.

Run probes from several regions that match where customers are, and require more than one failing location before declaring a broad outage, so one probe's network problem does not page everyone. Keep a separate, lower-threshold signal for "degraded in one region", because a real regional problem is exactly what a strict quorum hides.

Probes measure a sample of paths at a sample of times. Combine them with your application's own request success rate, which covers every real request.

Measure availability the way the contract defines it

Availability can be measured as the share of minutes in which the service was "up", or as the share of requests that succeeded. The two give different numbers for the same month, and an SLA must say which one it uses. It should also define:

  • what counts as unavailable (errors only, or also responses slower than a threshold);
  • excluded time, such as announced maintenance or outages caused by the customer;
  • whether availability is per tenant, per region, or service-wide;
  • how a customer claims a credit, by when, and the maximum credit.

Lab: per-minute or per-request?

The choice is not a technicality. The second program simulates one month of request logs for a tenant whose traffic is low at night and high in the afternoon, with three incidents: an hour at 03:00 where 20% of requests fail, nine minutes at 14:00 where every request fails, and 45 minutes at 10:00 where 4% fail. It computes availability per minute (a minute is down if more than 5% of its requests failed) and per request. Run it with go run ..

// Lab: one month, three incidents, two definitions of availability.
//
// A 30-day month of simulated request logs for one tenant: traffic follows a
// daily curve (quiet at night). Three incidents happen:
//
//	A. 60 minutes at 03:00 local time where 20% of requests fail (partial outage, quiet hours)
//	B. 9 minutes at 14:00 where every request fails (full outage, busy hours)
//	C. 45 minutes at 10:00 where 4% of requests fail (degradation below the 5% threshold)
//
// Availability is computed two ways an SLA might define it:
//
//	per minute:  a minute is "down" if more than 5% of its requests failed
//	per request: successful requests / all requests
package main

import (
	"fmt"
	"math"
)

func main() {
	const days, minutesPerDay = 30, 1440
	type minute struct{ total, failed float64 }
	month := make([]minute, days*minutesPerDay)
	for i := range month {
		hour := float64(i%minutesPerDay) / 60
		rate := 200 + 1800*math.Max(0, math.Sin(math.Pi*(hour-6)/14)) // req/min, peak mid-afternoon
		month[i] = minute{total: rate}
	}
	incident := func(day, startMin, length int, failShare float64) {
		for m := 0; m < length; m++ {
			i := day*minutesPerDay + startMin + m
			month[i].failed = month[i].total * failShare
		}
	}
	incident(3, 3*60, 60, 0.20)   // A
	incident(12, 14*60, 9, 1.00)  // B
	incident(20, 10*60, 45, 0.04) // C

	var downMinutes, total, failed float64
	for _, m := range month {
		total += m.total
		failed += m.failed
		if m.failed/m.total > 0.05 {
			downMinutes++
		}
	}
	perMinute := 100 * (1 - downMinutes/float64(len(month)))
	perRequest := 100 * (1 - failed/total)
	band := func(a float64) string {
		switch {
		case a >= 99.9:
			return "meets 99.9%"
		case a >= 99.0:
			return "below 99.9%, at least 99.0%"
		}
		return "below 99.0%"
	}
	fmt.Printf("requests in month: %.0f, failed: %.0f\n", total, failed)
	fmt.Printf("minutes counted as down (>5%% failed): %.0f of %d\n\n", downMinutes, len(month))
	fmt.Printf("per-minute availability:  %.4f%%  -> %s\n", perMinute, band(perMinute))
	fmt.Printf("per-request availability: %.4f%%  -> %s\n", perRequest, band(perRequest))

	fmt.Println("\nContribution of each incident")
	for _, inc := range []struct {
		name            string
		day, start, len int
	}{{"A  60 min, 20% failing, 03:00", 3, 180, 60}, {"B   9 min, 100% failing, 14:00", 12, 840, 9}, {"C  45 min, 4% failing, 10:00", 20, 600, 45}} {
		var d, f float64
		for m := 0; m < inc.len; m++ {
			x := month[inc.day*minutesPerDay+inc.start+m]
			if x.failed/x.total > 0.05 {
				d++
			}
			f += x.failed
		}
		fmt.Printf("  %-32s down minutes %3.0f   failed requests %7.0f\n", inc.name, d, f)
	}
}

Output recorded on November 3, 2026 (Go 1.26.5, windows/amd64):

requests in month: 37517039, failed: 22985
minutes counted as down (>5% failed): 69 of 43200

per-minute availability:  99.8403%  -> below 99.9%, at least 99.0%
per-request availability: 99.9387%  -> meets 99.9%

Contribution of each incident
  A  60 min, 20% failing, 03:00    down minutes  60   failed requests    2400
  B   9 min, 100% failing, 14:00   down minutes   9   failed requests   17537
  C  45 min, 4% failing, 10:00     down minutes   0   failed requests    3047

The same month passes or fails depending on the definition. Per minute it is 99.84%, below 99.9%, so a 99.9% SLA would owe a credit. Per request it is 99.94%, and nothing is owed. The night-time partial outage cost 60 full minutes but few requests; the short afternoon outage cost few minutes but most of the failed requests; the 4% degradation counted as zero down minutes under a 5% threshold even though 3,047 requests failed. Neither definition is wrong. Choose one deliberately, write it in the agreement, and report with exactly that definition.

Record incidents in an append-only log with start and end times, affected components, regions, and tenants, and with corrections as new entries that reference the original. That record is what you will calculate credits from, and what you will show a customer who disputes the number.

Calculate credits as proposals

Automate the calculation, not the payment. For each tenant and month, look up the agreement version that applied, compute availability by that agreement's definition, find the matching band, and produce a credit proposal that lists the inputs: measurement window, excluded intervals, availability, band, eligible fees, and amount. A person in finance or support reviews and applies it. This keeps the calculation reproducible while respecting that contracts differ and exceptions happen.

Where to start

Read your SLA's credit table and check whether every possible availability value falls into exactly one band, then confirm your reporting uses actual month lengths. Next, turn off your application in a staging environment and try to update the status page. If either check fails, you have found the two problems customers notice first. The observability guide covers finding which tenants an incident affected, and the capacity planner can help size headroom for the objective you choose.

Frequently Asked Questions

Should availability be measured in minutes or requests?

Request-based measurement reflects what users experienced and handles partial outages well; minute-based is easier for customers to verify. Either works if the SLA defines it, and your reports use the same definition.

Should a status page update automatically from monitoring?

Automated detection can open an incident, but a person should usually confirm and word the public message. Automatic public posts from noisy probes erode trust quickly.

Do scheduled maintenance windows count as downtime?

Only if the SLA says so. Many agreements exclude announced maintenance with a minimum notice period. Write the exclusion and notice rules down and apply them in your calculation.

Should credits be automatic or claimed?

That is a contract decision. Automatic credits build trust and cost more; claimed credits are common in standard terms. Either way, calculate proposed credits for every tenant so you know your exposure.

Is 99.99% a good SLA target for a small team?

It allows about four and a half minutes of downtime in a 31-day month, which is less than the time it takes most teams to notice, respond, and deploy a fix. Choose a target you can meet with your current on-call and deployment practices.

Sources

Documents cited in this guide. Each link was opened and its title compared with the label below on the date shown. A source supports the specific statements it is linked from; design recommendations and lab results are Velynoriva's own.

Revision history

  • — Added a reproducible lab for downtime budgets by month length and a gap in a common credit table, removed that gapped table from the guide, and explained per-request versus per-minute measurement. Simplified source labels. Added a second lab comparing per-minute and per-request availability for the same month.
  • — Corrected uptime math and made remedies agreement-specific.