Queue Metrics & CDR
Every queue call writes one row to a durable CDR timing layer in the PresenceEngine, and the standard contact-center KPIs (ASA, service level, abandon rate, and AHT) are derived from those four timestamps. As with agent KPIs, there’s no separate aggregation job: the metrics are a pure read over the CDR within a time window.
The queue CDR
Section titled “The queue CDR”CREATE TABLE queue_cdr( call_id TEXT PRIMARY KEY, queue_id TEXT, agent_id TEXT, -- who answered (NULL until answered) enqueued_at INTEGER, -- entered the queue (epoch ms) connected_at INTEGER, -- answered by an agent (NULL until answered) ended_at INTEGER, -- talk ended (NULL until the call ends) disposition TEXT -- offered | answered | abandoned | timeout);One row per offered call, written and updated across the call’s life by the same single-threaded brain that runs the ACD fence:
| Lifecycle event | Effect on the row |
|---|---|
| Enqueue | Insert with enqueued_at and disposition = offered. |
| Answer (agent confirms) | Set connected_at, agent_id, disposition = answered. |
| Abandon / timeout (before answer) | Set disposition = abandoned or timeout and ended_at. |
| Hang-up (after answer) | Set ended_at (the talk-end). |
The three timestamps enqueued_at → connected_at → ended_at are all you need to derive every KPI below.
The KPIs
Section titled “The KPIs”All metrics report over [since, now] and share a service-level threshold parameter, slSec (default 20 seconds).
Volumes
Section titled “Volumes”offered = rows for the queue in the windowanswered = rows where disposition = answeredabandoned = rows where disposition ∈ { abandoned, timeout }Abandon rate
Section titled “Abandon rate”abandonRatePct = round( 100 × abandoned / offered ) // 0 if offered = 0ASA: Average Speed of Answer
Section titled “ASA: Average Speed of Answer”The mean wait before an agent answered, over answered calls:
wait = connected_at − enqueued_atasaMs = round( mean(wait over answered calls) ) // 0 if none answeredService level
Section titled “Service level”The share of all offered calls answered within slSec:
slMet = answered calls where (connected_at − enqueued_at) ≤ slSec×1000serviceLevelPct = round( 100 × slMet / offered ) // 0 if offered = 0Note the denominator is offered, not answered: an abandoned call counts against service level, which is the stricter (and standard contact-center) definition.
AHT: Average Handle Time
Section titled “AHT: Average Handle Time”The mean talk time over calls that have ended:
ahtMs = round( mean(ended_at − connected_at over answered calls with ended_at) )AHT here is talk time. It does not yet fold in the agent’s post-call wrap_up; that’s tracked separately in agent state-time and is a planned addition to AHT.
Reading the report
Section titled “Reading the report”Queue KPIs are read from the public API at GET /v1/queues (the queues scope):
{ "queues": [ { "queue": "q_support", "offered": 120, "answered": 108, "abandoned": 12, "abandonRatePct": 10, "asaMs": 8200, "serviceLevelPct": 84, "ahtMs": 254000 } ]}Historical rollups: GET /v1/reports/queues and GET /v1/metrics/queues
Section titled “Historical rollups: GET /v1/reports/queues and GET /v1/metrics/queues”The KPIs above are computed live, as a pure read over queue_cdr for whatever window you ask for. A separate per-account MetricsDO drains queue_cdr (and agent_event) on a 5-minute cron and folds it into durable rollup tables at 15-minute (q15) and daily (q1d) grain, dimensioned by queue. This is additive: the live endpoint and KPIs above are unchanged, and the rollups give you pre-aggregated history beyond the live window without re-walking raw CDR rows.
Rollup rows store numerator/denominator pairs (e.g. total wait time and answered count, rather than a pre-computed ASA), divided once at read time — this avoids a mean-of-means error from averaging already-averaged buckets.
Read the rollups from the public API at GET /v1/reports/queues (the reports scope). GET /v1/metrics/queues is an alias over the same rollup data:
curl 'https://api.sip.io/v1/reports/queues?since=1719500000000&grain=15m' -H 'x-api-key: sk_…'{ "queues": [ { "queue": "q_support", "grain": "15m", "bucket_start": 1719504900000, "offered": 42, "answered": 38, "abandoned": 4, "asaMs": 7900, "serviceLevelPct": 86, "ahtMs": 249000 } ]}Team × queue rollups: GET /v1/reports/teams
Section titled “Team × queue rollups: GET /v1/reports/teams”Agents can be organized into teams for reporting. GET /v1/reports/teams returns the same queue-timing rollups sliced by team, so you can compare how each team performs against the same queues rather than only per-agent or account-wide:
curl 'https://api.sip.io/v1/reports/teams?since=1719500000000' -H 'x-api-key: sk_…'Team CRUD (GET/POST /v1/agent-teams, GET/POST /v1/agent-teams/{id}) lives on the Agent KPIs page.
Live REST snapshots
Section titled “Live REST snapshots”Alongside the WebSocket-pushed wallboard, GET /v1/live/queues returns a point-in-time REST snapshot of queue health (waiting count, longest wait, SLA-at-risk, alerts) for consumers that want a poll instead of a socket — see Live Supervisor Wallboard for the full set.
Cold archive
Section titled “Cold archive”Once tiered out of the live rollup window, MetricsDO’s q15/q1d (and the agent-side aq15/as15/ap15) tables are archived as NDJSON to the same object-storage data lake used for call CDRs — 15-minute rollups after roughly 30 days, daily rollups after roughly 13 months. This is a separate archive path from the call-level CDR archive: it holds aggregated queue/agent metrics, not per-call records.
Roadmap
Section titled “Roadmap”- Configurable service-level target per queue (today
slSecdefaults to 20s at the report layer). - Wrap-up in AHT: folding post-call work into handle time.