Skip to content

First contact resolution (FCR)

How often do we solve the problem in a single interaction?

Current value (service case data): 62.5% - ! Watch. Open the case.

Field Value
Definition Resolved tickets that needed exactly one customer contact and were not reopened, divided by all resolved tickets.
Formula count(contacts = 1 AND NOT reopened) / count(resolved)
Unit %
Granularity Ticket; reported monthly by channel and queue.
Direction Higher is better ↑
Target and thresholds ≥ 70.0% (amber ≥ 60.0%)
Parameters –
Owner Service Desk Manager
Source Ticketing system (synthetic generator)
Input table tickets, columns status, contacts, reopened
Recommended chart bar - Comparison across channels, sorted, against the target line.
Related KPIs Reopen rate, Resolution time (median)

How to read it

Effort saved for the customer and the team. Compare by channel: chat and phone usually differ structurally, so set targets per channel.

Common misreadings

  • Counting a ticket as FCR and then letting it reopen double-counts success; reopened tickets are excluded here.
  • Pushing FCR by closing tickets prematurely shows up as a higher reopen rate - read them together.

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: 50.00%. Resolved with 1 contact and not reopened: #1 yes, #2 no (2 contacts), #3 no (reopened), #4 yes. 2 / 4 = 0.5.

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("first_contact_resolution")
def first_contact_resolution(df: pd.DataFrame) -> float:
    r = _resolved(df)
    return safe_ratio((r["contacts"].eq(1) & ~r["reopened"].astype(bool)).sum(), len(r))

Reference SQL (tested on SQLite):

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