How to Calculate SLA Uptime
Step-by-step tutorial on calculating SLA uptime and downtime with JavaScript and Python formulas, downtime tables, and real-world measurement patterns.
How to Calculate SLA Uptime
Your VP of Engineering just asked for last month's SLA compliance report. You have a monitoring tool that tells you there were three incidents — one lasting 12 minutes, one lasting 47 minutes, and one lasting 6 minutes. You know the SLA target is 99.9%. But you are not sure whether to use calendar days or business days, whether to count partial outages, or how to handle the 30-minute maintenance window that was scheduled in advance. The math itself is simple division. The hard part is deciding what numbers go into the formula. This tutorial walks through the formulas in JavaScript and Python, covers every edge case that matters in practice, and gives you a downtime reference table you can use in SLA negotiations.
The Core Formula
SLA uptime is the ratio of time your service was available to the total time in the measurement period:
Uptime % = ((Total Time - Downtime) / Total Time) * 100Or equivalently:
Uptime % = (Uptime / Total Time) * 100Where:
For a 30-day month:
If you had 65 minutes of total downtime:
That number falls below the 99.9% threshold (which allows only 43.2 minutes of downtime in a 30-day month), meaning the SLA was breached.
JavaScript Implementation
Here is a production-grade JavaScript implementation that handles the full complexity of real-world SLA calculations:
/**
* SLA Uptime Calculator
*
* Handles: full outages, partial outages, scheduled maintenance exclusion,
* business-hours-only SLA, and multi-tier compliance checking.
*/
class SlaCalculator {
/**
* @param {Object} config
* @param {Date} config.periodStart
* @param {Date} config.periodEnd
* @param {boolean} [config.excludeScheduledMaintenance=true]
* @param {boolean} [config.businessHoursOnly=false]
* @param {Object} [config.businessHours] - { start: 9, end: 17, days: [1,2,3,4,5] }
* @param {number} [config.partialOutageWeight=0.5] - Weight for partial outages (0-1)
*/
constructor(config) {
this.periodStart = config.periodStart;
this.periodEnd = config.periodEnd;
this.excludeScheduledMaintenance = config.excludeScheduledMaintenance ?? true;
this.businessHoursOnly = config.businessHoursOnly ?? false;
this.businessHours = config.businessHours ?? {
start: 9,
end: 17,
days: [1, 2, 3, 4, 5], // Monday-Friday
};
this.partialOutageWeight = config.partialOutageWeight ?? 0.5;
}
/**
* Calculate uptime from a list of incidents.
*
* @param {Array<Object>} incidents
* @param {Date} incidents[].start
* @param {Date} incidents[].end
* @param {string} incidents[].type - "full" | "partial" | "maintenance"
* @param {string} [incidents[].description]
* @returns {Object} SLA metrics
*/
calculate(incidents) {
let totalMs;
if (this.businessHoursOnly) {
totalMs = this._calculateBusinessHoursMs(
this.periodStart,
this.periodEnd
);
} else {
totalMs = this.periodEnd.getTime() - this.periodStart.getTime();
}
let downtimeMs = 0;
const processedIncidents = [];
for (const incident of incidents) {
// Skip scheduled maintenance if configured to exclude
if (this.excludeScheduledMaintenance && incident.type === "maintenance") {
processedIncidents.push({
...incident,
excluded: true,
reason: "scheduled maintenance",
effectiveDowntimeMs: 0,
});
continue;
}
// Clamp incident to measurement period
const incStart = Math.max(
incident.start.getTime(),
this.periodStart.getTime()
);
const incEnd = Math.min(
incident.end.getTime(),
this.periodEnd.getTime()
);
if (incEnd <= incStart) {
processedIncidents.push({
...incident,
excluded: true,
reason: "outside measurement period",
effectiveDowntimeMs: 0,
});
continue;
}
let effectiveMs;
if (this.businessHoursOnly) {
effectiveMs = this._calculateBusinessHoursMs(
new Date(incStart),
new Date(incEnd)
);
} else {
effectiveMs = incEnd - incStart;
}
// Apply partial outage weight
if (incident.type === "partial") {
effectiveMs *= this.partialOutageWeight;
}
downtimeMs += effectiveMs;
processedIncidents.push({
...incident,
excluded: false,
effectiveDowntimeMs: effectiveMs,
});
}
const uptimeMs = totalMs - downtimeMs;
const uptimePercent = totalMs > 0 ? (uptimeMs / totalMs) * 100 : 100;
return {
period: {
start: this.periodStart.toISOString(),
end: this.periodEnd.toISOString(),
totalMinutes: this._msToMinutes(totalMs),
businessHoursOnly: this.businessHoursOnly,
},
downtime: {
totalMinutes: this._msToMinutes(downtimeMs),
formatted: this._formatDuration(downtimeMs),
incidents: processedIncidents,
},
uptime: {
totalMinutes: this._msToMinutes(uptimeMs),
percent: Number(uptimePercent.toFixed(6)),
formatted: uptimePercent.toFixed(4) + "%",
},
compliance: {
"99%": { met: uptimePercent >= 99, allowed: this._msToMinutes(totalMs * 0.01) + " min" },
"99.5%": { met: uptimePercent >= 99.5, allowed: this._msToMinutes(totalMs * 0.005) + " min" },
"99.9%": { met: uptimePercent >= 99.9, allowed: this._msToMinutes(totalMs * 0.001) + " min" },
"99.95%": { met: uptimePercent >= 99.95, allowed: this._msToMinutes(totalMs * 0.0005) + " min" },
"99.99%": { met: uptimePercent >= 99.99, allowed: this._msToMinutes(totalMs * 0.0001) + " min" },
},
errorBudget: {
"99.9%": {
totalMinutes: this._msToMinutes(totalMs * 0.001),
usedMinutes: this._msToMinutes(downtimeMs),
remainingMinutes: this._msToMinutes(Math.max(0, totalMs * 0.001 - downtimeMs)),
percentUsed: totalMs > 0
? Number(((downtimeMs / (totalMs * 0.001)) * 100).toFixed(1))
: 0,
},
},
};
}
_calculateBusinessHoursMs(start, end) {
let totalMs = 0;
const current = new Date(start);
while (current < end) {
const dayOfWeek = current.getUTCDay();
if (this.businessHours.days.includes(dayOfWeek)) {
const dayStart = new Date(current);
dayStart.setUTCHours(this.businessHours.start, 0, 0, 0);
const dayEnd = new Date(current);
dayEnd.setUTCHours(this.businessHours.end, 0, 0, 0);
const overlapStart = Math.max(current.getTime(), dayStart.getTime());
const overlapEnd = Math.min(end.getTime(), dayEnd.getTime());
if (overlapEnd > overlapStart) {
totalMs += overlapEnd - overlapStart;
}
}
// Move to the next day
current.setUTCDate(current.getUTCDate() + 1);
current.setUTCHours(0, 0, 0, 0);
}
return totalMs;
}
_msToMinutes(ms) {
return Number((ms / 60000).toFixed(1));
}
_formatDuration(ms) {
const totalSeconds = Math.floor(ms / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (days > 0) parts.push(${days}d);
if (hours > 0) parts.push(${hours}h);
if (minutes > 0) parts.push(${minutes}m);
if (seconds > 0 || parts.length === 0) parts.push(${seconds}s);
return parts.join(" ");
}
}
// ── Example Usage ──────────────────────────────────────────
const calculator = new SlaCalculator({
periodStart: new Date("2026-06-01T00:00:00Z"),
periodEnd: new Date("2026-07-01T00:00:00Z"),
excludeScheduledMaintenance: true,
partialOutageWeight: 0.5,
});
const result = calculator.calculate([
{
start: new Date("2026-06-05T02:00:00Z"),
end: new Date("2026-06-05T02:12:00Z"),
type: "full",
description: "Database failover during upgrade",
},
{
start: new Date("2026-06-15T14:00:00Z"),
end: new Date("2026-06-15T14:47:00Z"),
type: "full",
description: "API gateway misconfiguration after deploy",
},
{
start: new Date("2026-06-20T08:00:00Z"),
end: new Date("2026-06-20T08:06:00Z"),
type: "full",
description: "DNS propagation delay",
},
{
start: new Date("2026-06-25T03:00:00Z"),
end: new Date("2026-06-25T03:30:00Z"),
type: "maintenance",
description: "Scheduled database maintenance",
},
]);
console.log(JSON.stringify(result, null, 2));Python Implementation
For teams that prefer Python, here is the equivalent implementation:
"""
SLA Uptime Calculator — Python implementation.
Handles full outages, partial outages, scheduled maintenance exclusion,
and multi-tier compliance checking.
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
@dataclass
class Incident:
start: datetime
end: datetime
type: str # "full", "partial", "maintenance"
description: str = ""
@dataclass
class SlaConfig:
period_start: datetime
period_end: datetime
exclude_maintenance: bool = True
partial_outage_weight: float = 0.5
def calculate_sla(config: SlaConfig, incidents: list[Incident]) -> dict:
"""
Calculate SLA uptime from incident data.
Args:
config: SLA calculation parameters
incidents: List of incident records
Returns:
Dictionary with uptime metrics, compliance status, and error budget
"""
total_seconds = (config.period_end - config.period_start).total_seconds()
downtime_seconds = 0.0
processed_incidents = []
for incident in incidents:
# Skip scheduled maintenance if configured
if config.exclude_maintenance and incident.type == "maintenance":
processed_incidents.append({
"description": incident.description,
"excluded": True,
"reason": "scheduled maintenance",
"effective_downtime_seconds": 0,
})
continue
# Clamp to measurement period
inc_start = max(incident.start, config.period_start)
inc_end = min(incident.end, config.period_end)
if inc_end <= inc_start:
continue
effective_seconds = (inc_end - inc_start).total_seconds()
# Apply partial outage weight
if incident.type == "partial":
effective_seconds *= config.partial_outage_weight
downtime_seconds += effective_seconds
processed_incidents.append({
"description": incident.description,
"type": incident.type,
"start": inc_start.isoformat(),
"end": inc_end.isoformat(),
"effective_downtime_seconds": effective_seconds,
})
uptime_seconds = total_seconds - downtime_seconds
uptime_percent = (uptime_seconds / total_seconds) * 100 if total_seconds > 0 else 100
# Downtime allowances per SLA tier
sla_tiers = {
"99%": 0.01,
"99.5%": 0.005,
"99.9%": 0.001,
"99.95%": 0.0005,
"99.99%": 0.0001,
"99.999%": 0.00001,
}
compliance = {}
for tier_name, tier_fraction in sla_tiers.items():
allowed_downtime = total_seconds * tier_fraction
compliance[tier_name] = {
"met": uptime_percent >= float(tier_name.rstrip("%")),
"allowed_minutes": round(allowed_downtime / 60, 1),
"used_minutes": round(downtime_seconds / 60, 1),
"remaining_minutes": round(max(0, allowed_downtime - downtime_seconds) / 60, 1),
}
return {
"period": {
"start": config.period_start.isoformat(),
"end": config.period_end.isoformat(),
"total_hours": round(total_seconds / 3600, 1),
},
"uptime": {
"percent": round(uptime_percent, 6),
"formatted": f"{uptime_percent:.4f}%",
},
"downtime": {
"total_minutes": round(downtime_seconds / 60, 1),
"formatted": format_duration(downtime_seconds),
"incidents": processed_incidents,
},
"compliance": compliance,
}
def format_duration(seconds: float) -> str:
"""Format seconds into human-readable duration."""
total = int(seconds)
days, remainder = divmod(total, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, secs = divmod(remainder, 60)
parts = []
if days: parts.append(f"{days}d")
if hours: parts.append(f"{hours}h")
if minutes: parts.append(f"{minutes}m")
if secs or not parts: parts.append(f"{secs}s")
return " ".join(parts)
── Example ──────────────────────────────────────────────────
if __name__ == "__main__":
config = SlaConfig(
period_start=datetime(2026, 6, 1),
period_end=datetime(2026, 7, 1),
exclude_maintenance=True,
partial_outage_weight=0.5,
)
incidents = [
Incident(
start=datetime(2026, 6, 5, 2, 0),
end=datetime(2026, 6, 5, 2, 12),
type="full",
description="Database failover",
),
Incident(
start=datetime(2026, 6, 15, 14, 0),
end=datetime(2026, 6, 15, 14, 47),
type="full",
description="API gateway misconfiguration",
),
Incident(
start=datetime(2026, 6, 20, 8, 0),
end=datetime(2026, 6, 20, 8, 6),
type="full",
description="DNS propagation delay",
),
Incident(
start=datetime(2026, 6, 25, 3, 0),
end=datetime(2026, 6, 25, 3, 30),
type="maintenance",
description="Scheduled database maintenance",
),
]
result = calculate_sla(config, incidents)
print(f"Uptime: {result['uptime']['formatted']}")
print(f"Downtime: {result['downtime']['formatted']}")
print(f"99.9% SLA met: {result['compliance']['99.9%']['met']}")
print(f"Error budget remaining: {result['compliance']['99.9%']['remaining_minutes']} min")The Downtime Reference Table
Use this table during SLA negotiations. It shows the maximum allowed downtime for each SLA tier across different measurement periods:
| SLA Tier | Per Year | Per Quarter | Per Month (30d) | Per Week | Per Day | |----------|----------|-------------|-----------------|----------|---------| | 99% | 3d 15h 39m | 21h 54m 43s | 7h 18m 17s | 1h 40m 48s | 14m 24s | | 99.5% | 1d 19h 49m | 10h 57m 21s | 3h 39m 8s | 50m 24s | 7m 12s | | 99.9% | 8h 45m 36s | 2h 11m 24s | 43m 49s | 10m 4s | 1m 26s | | 99.95% | 4h 22m 48s | 1h 5m 42s | 21m 54s | 5m 2s | 43.2s | | 99.99% | 52m 35s | 13m 8s | 4m 23s | 1m 0s | 8.6s | | 99.999% | 5m 15s | 1m 18s | 26.3s | 6.0s | 0.86s |
Print this table and pin it next to your monitor. You will reference it every time someone says "we need five nines."
Edge Cases That Trip Up SLA Calculations
Edge Case 1: Months Have Different Lengths
A 99.9% SLA allows 43.8 minutes in a 30-day month, 44.6 minutes in a 31-day month, and 40.3 minutes in February (non-leap year). Always calculate against the actual number of minutes in the specific month, not an average.
Edge Case 2: Overlapping Incidents
If two incidents overlap in time — for example, a partial outage from 2:00-3:00 PM and a full outage from 2:30-2:45 PM — you need to merge them. The overlapping period should count at the higher severity level:
function mergeIncidents(incidents) {
// Sort by start time
const sorted = [...incidents].sort(
(a, b) => a.start.getTime() - b.start.getTime()
);
const merged = [];
let current = { ...sorted[0] };
for (let i = 1; i < sorted.length; i++) {
const next = sorted[i];
if (next.start.getTime() <= current.end.getTime()) {
// Overlapping — extend current and use higher severity
current.end = new Date(
Math.max(current.end.getTime(), next.end.getTime())
);
// Full outage takes precedence over partial
if (next.type === "full") current.type = "full";
} else {
merged.push(current);
current = { ...next };
}
}
merged.push(current);
return merged;
}Edge Case 3: Timezone Boundaries
If your SLA period is "calendar month in US Eastern time" but your monitoring data is in UTC, the period boundaries shift by 4-5 hours depending on daylight saving time. Always normalize to a consistent timezone before calculating.
Edge Case 4: Planned vs. Unplanned Downtime
Most SLA contracts define this differently. Common approaches:
Read your SLA contract carefully. The exclusion clause is often the most important paragraph.
Edge Case 5: Partial Outages
A service returning 500 errors for 30% of requests is not "down" in the total-outage sense, but it is not "up" either. Common approaches:
The JavaScript and Python implementations above use the weighted approach with a configurable weight factor.
Automating SLA Reports
Instead of manually calculating SLA compliance each month, automate it:
// Generate monthly SLA report using PingCheck data
async function generateMonthlySlaReport(year, month) {
const periodStart = new Date(Date.UTC(year, month - 1, 1));
const periodEnd = new Date(Date.UTC(year, month, 1));
// Fetch incidents from PingCheck
const response = await fetch(
https://api.luxkern.com/v1/pingcheck/incidents? +
start=${periodStart.toISOString()}&end=${periodEnd.toISOString()},
{
headers: { Authorization: "Bearer YOUR_API_KEY" },
}
);
const { incidents } = await response.json();
// Map to calculator format
const mappedIncidents = incidents.map((inc) => ({
start: new Date(inc.startedAt),
end: new Date(inc.resolvedAt || periodEnd.toISOString()),
type: inc.severity === "major" ? "full" : "partial",
description: inc.title,
}));
// Calculate
const calculator = new SlaCalculator({
periodStart,
periodEnd,
excludeScheduledMaintenance: true,
partialOutageWeight: 0.5,
});
return calculator.calculate(mappedIncidents);
}
// Run for June 2026
generateMonthlySlaReport(2026, 6).then((report) => {
console.log(June 2026 SLA Report);
console.log(Uptime: ${report.uptime.formatted});
console.log(Downtime: ${report.downtime.formatted});
console.log(99.9% SLA: ${report.compliance["99.9%"].met ? "MET" : "BREACHED"});
});Communicating SLA Numbers
How you communicate uptime numbers matters as much as the numbers themselves.
To Customers (Status Page)
Show the trailing 90-day uptime percentage, updated in real-time. Display individual component uptimes (API, Dashboard, Webhooks). Do not hide behind aggregate numbers — if the API was down for 2 hours but the dashboard was fine, show that.
To Internal Stakeholders
Show the error budget. "We have used 67% of our 99.9% error budget this month with 8 days remaining" is more actionable than "Our uptime is 99.93%."
In SLA Contracts
Be specific about measurement methodology, exclusions, and credit terms. A vague SLA invites disputes. A precise SLA sets clear expectations for both parties.
Try PingCheck free — no credit card required.
Summary
SLA uptime calculation is simple arithmetic wrapped in complex business rules. The formula is division. The difficulty is in defining what counts as downtime, handling edge cases, and automating the reporting so it happens consistently every month.
Use the JavaScript or Python implementations above as a starting point. Customize the incident weighting, maintenance exclusion, and business hours logic to match your specific SLA contract.
For a practical understanding of what these percentages mean in real downtime, read our guide on what 99.9% uptime really means. For setting up the monitoring that feeds these calculations, see our introduction to uptime monitoring.