A call center’s lists are its inventory, and almost nobody can tell you how theirs are performing.
An acquisition list is 500,000+ leads worked across ten attempts. Inside that are the questions that decide whether the operation makes its month:
- Which regions actually perform, and which ones absorb dials?
- Which caller IDs are working, and are any of them over- or under-indexing?
- What is the best time of day to reach a customer, by region?
- What is the best attempt — where does the return stop justifying the dial?
- How often are we rotating caller IDs, and does that rotation match the region we are calling?
These are the lifeblood of a floor trying to get the most out of a list. They’re all answerable from data that already exists. Every dial writes a row.
There was no good way to see any of it.
Twenty million rows and no view
The attempts table holds roughly twenty million rows and grows every month. None of that was ever the hard part. Answering any of the questions above meant someone writing a query, and a question you have to commission is a question you stop asking.
So I built the view.
The constraint was fixed before I started: SQL Server Express, which enforces a hard ten-gigabyte ceiling per database. Not a licensing nag. A wall. Page compression is the only reason it fits, and compressed, the data file sits around 3.5GB of the ten available. Uncompressed it doesn’t fit at all, and moving to a larger edition was a procurement conversation nobody was going to have.
Express also runs one memory-grant pool, so parallel cold aggregates starve each other. That one fact shapes more of the architecture than any design preference did.
It’s the honest shape of most real infrastructure work. Nobody chose this stack. The engineering was making something genuinely useful exist inside constraints handed to me.
What it found
Contact rates had been slipping at every attempt. Not a cliff. A decay, across the board, with nothing to substantiate it. Every explanation available was a guess, and none of them could be tested.
The first real pass through caller-ID rotation showed one program sending nearly all of its dials through a single number.
That’s not a cosmetic problem. Carriers watch call patterns, and a number behaving that way gets flagged as spam, at which point answer rates fall for reasons that never appear in any report you own. We held STIR/SHAKEN Attestation A, the highest there is, the carrier vouching that we had the right to use the numbers we called from. It still isn’t a shield. Attestation establishes who you are and says nothing about whether you’re behaving well, so the diligence stays yours.
We believe we were being quietly side-lined, and the decay in contact rate was what that looked like from the inside.
Every one of those dials was a row in a table anyone could have queried. Nobody could see it. Seeing it needed a view of rotation health by attempt and by region, and nobody had built one.
Caching, and being wrong about it
These views read the whole table rather than a slice of it. The heaviest one reads every month at once, four queries across twenty million attempts, and cold it takes about sixteen minutes. Warm, the same view returns in about half a second, and the standard views can be pre-loaded before anyone asks for them. Caching is the product here, not an optimization sitting on top of one.
That ratio is the argument for building it this way. Sixteen minutes is long enough that people quietly stop asking, which puts the floor back where it started, with answers sitting in a table nobody goes and gets.
Cache entries key against per-month version fingerprints. Ingesting new dialer data bumps only the months that ingest actually touched. Replacing disposition mappings bumps everything, because those flags ripple backwards through history. Historical months, once settled, effectively never recompute.
A warming job pre-computes the standard views after each ingest, so nobody’s Monday morning is the slow one.
It was silently warming nothing.
The cache key hashes the request, and parameter order is part of it. The warming job built its URLs by hand. The application built its from its own parameter builders, and the two produced different strings for identical requests. Every warm run diligently populated keys no user would ever hit, and every user still ate the cold compute.
Nothing errored and nothing logged. The job reported success because it had genuinely done what it was told.
The fix was to make the warmer emit application-identical strings byte for byte. I keep relearning this one: a job that reports success is not evidence that the work happened. Verify the effect, not the exit code.
The things that won’t hold still
One client renumbers its campaign identifiers every quarter, so goals arrive tagged with identifiers that may no longer correspond to anything currently dialing. Rather than hand-mapping that every quarter, the sync resolves each goal row through campaign identity to whichever jobs actually dialed that month, landing the goal once and creating zero-goal placeholders for its siblings so nothing double-counts.
Anything it can’t resolve lands in one of two report lists: goal rows represented nowhere, and dialed work no goal accounts for. Both lists have to come back empty before a sync counts as done. The system knows what it doesn’t know and says so, rather than bucketing the remainder into “Unknown” and letting a wrong number reach a client deck.
The platform also carries two month bases at once, fiscal and calendar, because the clients on it don’t agree about when a month starts. It tracks both and every aggregate declares which one it used. A report that doesn’t tie out to the numbers a client already audits is worthless no matter how good it is.
Where it stopped
I was furloughed before this one was finished.
The rotation finding was the first pass, not the last. Local versus toll-free by region, rotation depth by attempt, whether the best time to call holds per region or only in aggregate: all answerable now, none of them answered yet.
I’m including it anyway. What I’d want evaluated is the list of five questions at the top. Whether those are the right ones to ask of a dialer was a judgment I had to make before any of this got built, and no tooling was going to make it for me.