Case study 04 · Real-time & analysis

Measure before you build

Context
The call-centre platform of an enterprise client, built on a commercial phone system
My role
Main contributor since mid-2026 on a system a larger team built before me.
When
2026 – now
Stack
.NET · SQL Server · Redis · SignalR · MassTransit · Azure Service Bus

The problem

Too many callers were hanging up in the queue. The obvious fixes on the table were more staff, or a new routing feature the business had already asked for. Before building anything, I wanted to know why it was actually happening.

What I did

  • Made the data trustworthy first. The phone vendor’s API was throttling statistics requests. I batched and parallelised them, cached them in Redis by day with a fallback when Redis is unavailable, retried throttled calls, and added the indexes the queries needed. I also added a nightly sync of abandoned calls so the data team could work from the same numbers.
  • Investigated the cause. I looked at every claim about the problem and labelled each one measured, inferred or refuted, using standard queueing maths to check what staffing could and couldn’t explain.
  • Found it. The biggest cause wasn’t staffing or skills. The application’s own status logic was logging agents out of their queues.
  • Pushed back with evidence. A feasibility report showed the requested routing feature couldn’t be built as specified, and would have addressed somewhere between 0.1% and 2% of abandoned calls.
  • Fixed what mattered. Recording which queue each call came through (with a backfill), requiring an outcome before an agent goes back to ready, a flag-gated fallback queue, an open/closed status, and reserving a line before it rings with a Redis gate that lets calls through if Redis fails.

Decisions and tradeoffs

  • Evidence over momentum. Saying “don’t build the thing you asked for” only works with numbers behind it, and with a better option on the table.
  • Fail open where callers are involved. Every new guard in the call path lets the call through if its own dependency is down. A missed optimisation is better than a dropped call.
  • Flags for anything that changes routing. New behaviour ships switched off and turns on deliberately.

Alongside it

I also took over an AI voice receptionist that answers and routes calls: measuring the delay callers actually hear, routing only to queues that are open, and spoken handoffs before every transfer.

phone systemcommercialqueuesagentsstatus logicthe app's ownlogs agents out← the main causestatsbatched · cached · retriednightly syncabandoned callsanalysismeasured · inferredrefutedtargeted fixesstatus · outcome · fallbackrequested feature≈ 0.1–2% of abandons① vendor API was throttling stats② redis down? fall back③ every claim labelled before any build④ guards fail open
my partexisting
Original, simplified diagram. Not the real system.

Switch on “Show working” for the annotations.