Call-centre AHT and FCR: define a repeat contact before you count it
In this article (6 sections)
Related programme: the Data Analytics with Generative AI course (3–4 months; ₹90,000 (inclusive of GST) · EMI from ₹4,000/month).
First call resolution (FCR) is the share of contacts not followed by another contact from the same customer about the same issue within a fixed window, and it can only be calculated once four choices are written down: the window length, whether the repeat must be the same issue, which channels count as a repeat, and how transferred calls are grouped. On the same log those choices can move FCR by almost twenty points. Average handle time (AHT) has a matching choice: per call leg or per customer contact.
The operations question behind the metric
The head of a care operation, whether in-house at a telecom or at an outsourcing partner in Noida or Gurugram, is usually trying to decide between two pressures: the client contract rewards short handle time and high FCR, and the two pull against each other. Before anyone can say whether a team is trading one for the other, the analyst has to make the metrics mean the same thing every week. That is the whole job of this playbook. (If you are an agent thinking of moving into this analyst seat, our BPO to data analyst guide covers that career step; this article is about the measurement itself.)
Decisions to record before the first query
| Decision | Options | What changes |
|---|---|---|
| Repeat window | 24 hours, 3, 7 or 14 days | longer windows catch more repeats and lower FCR |
| Issue matching | any contact vs same issue code | "any" treats an unrelated later query as a failure |
| Channel scope | voice only vs voice, chat, email, app | customers who switch to chat after a failed call vanish under voice-only |
| Transfers | each leg a contact vs legs merged into one | legs inflate volume and deflate AHT |
| Direction | inbound only vs include outbound callbacks | an agent's promised callback is not a customer repeat |
Write the chosen row into the report footer. If the client contract defines FCR, use the contract's definition for billing and label any other version as internal.
The log, and the queries
A synthetic week for a telecom care desk: eight customers, thirteen contacts, one transferred call (contact 3 has two legs), one customer who moved to chat and one who followed up by email.
| Column | Meaning |
|---|---|
| contact_id, leg | a transferred call keeps its contact_id and gets leg 2 |
| customer_id | account or verified phone number, not caller ID |
| channel | voice, chat, email |
| issue_code | disposition chosen by the agent at wrap-up |
| talk_s, hold_s, acw_s | talk, hold and after-call work in seconds |
-- Synthetic contact log for a telecom care desk. Durations in seconds.
CREATE TABLE contacts (
contact_id INTEGER, leg INTEGER, customer_id TEXT, channel TEXT,
started_at TEXT, issue_code TEXT, agent_id TEXT,
talk_s INTEGER, hold_s INTEGER, acw_s INTEGER
);
INSERT INTO contacts VALUES
(1,1,'C1','voice','2026-09-01 10:05','BILLING','AG1',240,30,60),
(2,1,'C1','voice','2026-09-02 18:40','BILLING','AG2',300,0,45),
(3,1,'C2','voice','2026-09-01 11:10','DATA','AG1',180,20,40),
(3,2,'C2','voice','2026-09-01 11:15','DATA','AG3',420,60,90),
(4,1,'C3','voice','2026-09-01 12:00','SIM','AG2',200,0,50),
(5,1,'C3','chat','2026-09-03 09:30','SIM','AG4',600,0,30),
(6,1,'C4','voice','2026-09-02 14:00','BILLING','AG1',150,10,30),
(7,1,'C4','voice','2026-09-06 16:20','PLAN','AG3',260,40,70),
(8,1,'C5','voice','2026-09-02 15:00','DATA','AG2',330,90,60),
(9,1,'C5','voice','2026-09-12 10:00','DATA','AG1',280,30,50),
(10,1,'C6','voice','2026-09-03 10:00','PLAN','AG3',210,0,40),
(11,1,'C7','voice','2026-09-03 13:30','BILLING','AG4',190,20,45),
(12,1,'C7','email','2026-09-04 08:00','BILLING','AG4',0,0,420),
(13,1,'C8','voice','2026-09-04 17:45','SIM','AG2',160,0,35);
-- One row per contact: handle time summed across transfer legs, first agent kept
CREATE VIEW contact_level AS
SELECT contact_id, customer_id, channel, MIN(started_at) AS started_at, issue_code,
MAX(CASE WHEN leg = 1 THEN agent_id END) AS first_agent,
COUNT(*) AS legs,
SUM(talk_s + hold_s + acw_s) AS handle_s
FROM contacts GROUP BY contact_id, customer_id, channel, issue_code;
-- AHT two ways
SELECT 'per leg' AS basis, COUNT(*) AS n, ROUND(AVG(talk_s + hold_s + acw_s)) AS aht_s
FROM contacts WHERE channel = 'voice'
UNION ALL
SELECT 'per contact', COUNT(*), ROUND(AVG(handle_s))
FROM contact_level WHERE channel = 'voice';
-- FCR under three definitions, all with a 7-day window, voice contacts as the base
WITH nxt AS (
SELECT c.*,
(SELECT MIN(d.started_at) FROM contact_level d
WHERE d.customer_id = c.customer_id AND d.started_at > c.started_at
AND d.started_at <= datetime(c.started_at, '+7 days')
AND d.channel = 'voice') AS any_voice_repeat,
(SELECT MIN(d.started_at) FROM contact_level d
WHERE d.customer_id = c.customer_id AND d.started_at > c.started_at
AND d.started_at <= datetime(c.started_at, '+7 days')
AND d.channel = 'voice' AND d.issue_code = c.issue_code) AS same_issue_voice,
(SELECT MIN(d.started_at) FROM contact_level d
WHERE d.customer_id = c.customer_id AND d.started_at > c.started_at
AND d.started_at <= datetime(c.started_at, '+7 days')
AND d.issue_code = c.issue_code) AS same_issue_any_channel
FROM contact_level c WHERE c.channel = 'voice'
)
SELECT COUNT(*) AS voice_contacts,
ROUND(100.0 * SUM(any_voice_repeat IS NULL) / COUNT(*), 1) AS fcr_any_voice_pct,
ROUND(100.0 * SUM(same_issue_voice IS NULL) / COUNT(*), 1) AS fcr_same_issue_voice_pct,
ROUND(100.0 * SUM(same_issue_any_channel IS NULL) / COUNT(*), 1) AS fcr_same_issue_all_channels_pct
FROM nxt;AHT is 320 seconds per leg across 12 voice legs, but 349 seconds per contact across 11 contacts, because the transferred call counts once with both legs' time. FCR comes out three ways:
| Definition (7-day window) | FCR |
|---|---|
| No later voice contact of any kind | 81.8% |
| No later voice contact on the same issue | 90.9% |
| No later contact on the same issue, any channel | 72.7% |
The same-issue voice figure looks best because it forgives C3, who gave up on the phone and opened a chat about the same SIM problem, and C7, who followed up by email. The any-issue figure penalises C4 for calling four days later about a different plan query. The cross-channel, same-issue definition is the one closest to what the customer experienced.
Who gets charged for the repeat
For coaching, a repeat is charged to the agent who handled the original contact:
-- Charge each same-issue, any-channel repeat to the agent who took the first contact
SELECT c.first_agent,
COUNT(*) AS voice_contacts,
SUM(EXISTS (SELECT 1 FROM contact_level d
WHERE d.customer_id = c.customer_id AND d.started_at > c.started_at
AND d.started_at <= datetime(c.started_at, '+7 days')
AND d.issue_code = c.issue_code)) AS repeated,
ROUND(AVG(c.handle_s)) AS aht_s,
SUM(c.legs > 1) AS transferred
FROM contact_level c WHERE c.channel = 'voice'
GROUP BY c.first_agent ORDER BY c.first_agent;AG1 shows an AHT of 423 seconds, the highest, but a large part is AG3's 570-second second leg on the transferred call. Contact-level AHT is right for the customer view and wrong for an agent's scorecard; agent AHT should be computed on the legs they handled. The table also gives AG4 a repeat for the follow-up email, which AG4 may have sent as a promised update. Without a direction column, the data cannot tell a customer chasing from an agent keeping a promise.
Things the dialler export will not tell you
- Disposition codes drift. If agents pick "General enquiry" for anything awkward, same-issue matching finds fewer repeats. Check the share of vague codes by agent before trusting issue-level FCR.
- Abandoned calls in IVR or queue are contacts the customer made. A customer who hangs up after four minutes in queue and calls again the next day created a repeat that no agent handled.
- Customer identity. Caller ID matches a phone, not an account. A customer calling from a family member's number escapes repeat detection.
- Window edge. C5 called again after ten days on the same data issue. A 7-day window counts contact 8 as resolved; a 14-day one does not. State the window, and show a second window as a sensitivity line.
Reporting to the operations head
Put one FCR on the page, with its full definition underneath in a single line, and keep AHT at contact level beside it. Then one short table: repeats split by channel of the repeat (voice, chat, email). That split says whether FCR problems are hidden by channel switching, which the voice-only figure cannot show. Agent-level numbers go to team leads, on legs, with contact counts visible so four calls are not read as a trend.
Churn often follows unresolved service issues; our telecom ARPU and churn playbook shows the revenue side. Correlated subqueries and window logic like the above are practised on operational datasets in the Data Analyst programme.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI