← Back to blog
pingcheck

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.

SLAuptimedowntimemonitoringdevops

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) * 100


Or equivalently:

Uptime % = (Uptime / Total Time) * 100


Where:
  • Total Time = the full measurement period (usually one calendar month)
  • Downtime = the sum of all periods where the service was below the agreed availability threshold
  • Uptime = Total Time - Downtime


  • For a 30-day month:
  • Total Time = 30 days = 720 hours = 43,200 minutes


  • If you had 65 minutes of total downtime:
  • Uptime = 43,200 - 65 = 43,135 minutes
  • Uptime % = (43,135 / 43,200) * 100 = 99.8495%


  • 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:

  • Exclude all planned maintenance — the most common. Scheduled windows do not count.
  • Exclude up to N hours/month — planned maintenance is excluded up to a cap (e.g., 4 hours/month).
  • Include everything — the strictest. Even planned maintenance counts against the SLA.


  • 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:

  • Weighted downtime — 30% error rate counts as 30% downtime for that period
  • Threshold-based — any error rate above 5% counts as full downtime
  • Severity tiers — partial outage weighted at 50%, degraded performance at 25%


  • 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.