Skip to content

SLA compliance

What share of tickets did we resolve within the time we promised?

Current value (service case data): 94.4% - ✓ On target. Open the case.

Field Value
Definition Resolved tickets whose resolution time is within the SLA target of their priority, divided by all resolved tickets.
Formula count(resolution_hours <= sla_target_hours) / count(resolved)
Unit %
Granularity Ticket; reported weekly and monthly by priority and queue.
Direction Higher is better ↑
Target and thresholds ≥ 90.0% (amber ≥ 85.0%)
Parameters –
Owner Service Desk Manager
Source Ticketing system (synthetic generator)
Input table tickets, columns status, resolution_hours, sla_target_hours
Recommended chart bullet - Value against target and amber band in one glance; per queue as small bullets.
Related KPIs Resolution time (median), Aged backlog (> 7 days)

How to read it

The promise kept. A drop means customers felt it already; look at aged backlog to see the drop coming two to three weeks earlier.

Common misreadings

  • A lagging indicator - it is measured at closure, so tickets breaching right now are not in it yet.
  • Pausing the SLA clock ("waiting for customer") without auditing the pauses inflates compliance.
  • An overall figure averages P1 and P4; a 95% fleet number can coexist with a broken P1 promise.

Worked example (from the test suite)

ticket_id status first_response_hours resolution_hours sla_target_hours contacts reopened age_days
1 resolved 1.0 10.0 24.0 1 False –
2 resolved 3.0 30.0 24.0 2 False –
3 resolved 0.5 4.0 8.0 1 True –
4 resolved 6.0 50.0 72.0 1 False –
5 open – – 24.0 1 False 9.0

Expected result: 75.00%. Within SLA: #1 10<=24 yes, #2 30<=24 no, #3 4<=8 yes, #4 50<=72 yes. 3 of 4 resolved = 0.75. The open ticket is not counted.

This example is asserted in tests/test_formulas.py against the Python function and in tests/test_sql.py against the SQL below.

Implementations

Python function (the single implementation used by the site):

@kpi("sla_compliance")
def sla_compliance(df: pd.DataFrame) -> float:
    r = _resolved(df)
    return safe_ratio((r["resolution_hours"] <= r["sla_target_hours"]).sum(), len(r))

Reference SQL (tested on SQLite):

SELECT CAST(SUM(CASE WHEN resolution_hours <= sla_target_hours THEN 1 ELSE 0 END) AS REAL) / COUNT(*)
FROM tickets WHERE status = 'resolved'