solomonmark.dev · a journal

Not Every Hang-Up Is a Lost Customer: Building Abandoned-Call Recovery on Amazon Connect

A caller waits on hold, and at ninety seconds, they hang up. In most contact-center reporting, that event gets filed under one label: lost lead. It goes into a dashboard tile, the tile turns red if it happens too often, and everyone moves on.

But two questions almost nobody asks about that hang-up: did this person call back later and get helped anyway? And was there even a person on the line, or was it a fax machine, a forwarding switch, or a spam dialer that happened to ring a business queue?

We spent several months building a system that answers both questions, on top of Amazon Connect, for a portfolio of business phone lines. The headline is not “we call abandoned callers back.” The headline is that we figured out, with real data, which abandoned callers are worth calling back — and which ones would be a waste of an agent’s time, or worse, an unwanted call to someone who was already helped. This post is the story of how we did that: the definition we standardized on, the metrics we had and the ones we needed, a four-step filter that turns tens of thousands of abandons into a short worklist, and what we do with that worklist once we have it — including how we set it up to run inside Amazon Connect itself, and how it becomes lead generation for the businesses we serve.


1. What is an abandoned call in a contact center?

Strip away the jargon and the definition is intuitive: an abandoned call is a call that reached a real queue, waited for an agent, and hung up before anyone answered it. That’s it. No agent touched it, and it wasn’t shuffled off somewhere else first.

We standardized this as one predicate, applied everywhere we count abandons, so every report in the business means the same thing:

queue reached  AND  no agent ever attached  AND  not routed onward

Each condition earns its place:

  • Queue reached — the call has to have actually queued for a live agent. A caller who hangs up while still listening to an opening greeting, before ever entering a queue, never had a chance to reach anyone — that’s a different failure mode, usually called a blocked call, and lumping it in with abandons hides which problem you actually have.
  • No agent attached — obvious, but worth stating precisely: nobody ever picked up.
  • Not routed onward — this is the conjunct people forget, and it’s the one that causes the most double-counting. If a call was transferred to another queue or another leg of the flow, the leg you’re looking at isn’t the end of the story — it might have been answered one hop later. Only count a leg as abandoned if it was the last thing that happened to that contact.

One thing we deliberately do not do: apply any time threshold. A two-second hang-up and a four-minute hang-up both count as abandoned, with no “short abandon” carve-out. That’s a defensible choice — a caller who waited four minutes and gave up is a much stronger signal than someone who dialed the wrong number and hung up instantly — but it means the raw abandon count mixes two very different populations together. Untangling those populations is most of what the rest of this post is about.


2. What is abandoned-call recovery, and why should a client care?

Abandoned-call recovery is the practice of taking that list of hang-ups, deciding which callers are genuinely worth reaching out to, and reaching out to them — instead of treating the abandon count as a number you report and move past.

For us, the “client” matters here in a specific way: we run contact centers on behalf of other businesses. When someone abandons a call to one of our client’s lines, that person is the client’s prospective customer, not ours. A missed call at a law firm is a missed consultation. A missed call at a clinic is a missed patient. A missed call at a home-services company is a missed job. The cost of an unrecovered abandon lands on the client’s business, not on the contact center’s KPI sheet — which is exactly why recovery is worth building as a product, not just as an internal metric.

The naive version of recovery — call every single abandoned number back — sounds appealing and fails in practice. Agent time is the scarce resource in any contact center, and a call-everyone-back policy spends it on:

  • forwarding switches and shared office lines that were never the actual caller,
  • callers who rang back on their own and were already helped,
  • spam, fax tones, and solicitors that never wanted to reach a human in the first place.

Beyond the wasted minutes, there’s a real cost to calling people who didn’t ask to be called back — annoyance, and in regulated contexts, compliance exposure. So the real deliverable of recovery isn’t a report that says “here are all your abandoned calls.” It’s a worklist: a short, specific list of callers where a callback is likely to land well, with the reason each one made the list attached.


3. Which metrics find abandoned callers?

There are two levels of abandon metrics, and the gap between them is the whole point of this project.

Aggregate metrics — what a standard Connect report gives you out of the box:

MetricWhat it tells you
Contacts abandonedRaw count of abandons in the window
Contacts queuedDenominator for an abandon rate against queue volume
Average queue abandon timeHow long, on average, someone waits before giving up
Service level 60/120 secondsShare of contacts answered inside a target window
Contacts abandoned in N seconds (buckets at 15/20/25/30/45/60/90/120/180/240/300/600s)Distribution of how quickly people give up
Contacts transferred out from queueVolume that left the queue via transfer, not abandonment

These are the right numbers for a queue-level dashboard, and they surface a real pattern in our own data: an abandon rate around 3.5%, an average wait of a bit over two minutes before someone hangs up, and — this is the important part — roughly 94% of abandons happening in under fifteen seconds. Read that carefully: the overwhelming majority of “abandoned calls” are instant hang-ups, not people who waited and gave up. The opportunity for recovery is concentrated in a minority of the abandon count, not spread evenly across it.

One denominator trap worth naming: abandoned-per-queued and abandoned-per-inbound-call are both defensible abandon rates, and they are not the same number. Pick one, label it clearly, and never let the two coexist unlabeled on the same dashboard — we’ve seen that exact confusion cause a “why doesn’t this reconcile” conversation more than once.

Caller-level metrics — what none of the above gives you: a phone number. Every metric above is a count over a queue or a time bucket. None of them tells you whose call to call back. For recovery, the unit of work has to be the pair: this calling number reached this business line and abandoned. Once you have that pair, you can ask a much more useful question — not “how many calls did we lose,” but “which specific relationships are worth re-opening.”


4. Where the caller-level data comes from — and what else you could use

Here’s our pipeline, described at the level that matters rather than by internal names: Amazon Connect emits a Contact Trace Record (CTR) for every leg of every contact. We stream those records through Kinesis to a processing function, which normalizes each record and writes it into a contact-record store. That store holds one row per call leg — the number called, the number calling, a timestamp, call direction, which queue it sat in and for how long, whether an agent was ever attached and for how long, and the message type and call conclusion the agent recorded, if any.

Two things about that shape matter more than anything else in this whole system:

  • We can cheaply pull every leg for one business line over a window — the raw abandon scan.
  • We can cheaply pull one caller’s entire history against that line — the recovery score.

Those two lookups are the entire engine. Everything from here on is arithmetic on top of them.

We didn’t start here, though, and it’s worth being honest about the alternatives and why each one fell short for this specific job:

SourceGood forWhere it falls short for recovery
Connect’s historical metrics report / metrics APITrend lines, queue league tables, SLA trackingAggregated by queue and time — no calling number, so no worklist
Connect’s real-time metrics APILive queue depth, “who’s waiting right now”No history and no lookback — useless for scoring past callers
Connect’s contact-search APIAd-hoc, contact-level lookupScoped to a queue rather than a line, and a queue-wide search over our full history exceeded our API gateway’s request timeout — our contact-record store answers the same per-line question in a few seconds
Contact records exported to a data lake with ad-hoc SQL over themCheap, deep history, flexible queryingBatch-latency, not built for point lookups on a single pair
Contact records rolled up into warehouse views (we also run this, for org-wide dashboards)Trend and rate reporting across the whole businessPre-aggregated by design — abandon is stored as a count, not as a row, so there’s no path back to an individual caller
Conversational analytics on recordingsSentiment, transcript quality on answered callsAn abandoned call has no conversation — there’s nothing to analyze

The rule of thumb we settled on: aggregates are for the dashboard, contact-level records are for the worklist. If a metric can’t be traced back to a specific caller, it can tell you that a problem exists, but it can never tell you who to call.


5. What actually makes a caller worth calling back?

Before any scoring logic, state the hypothesis in plain words. An abandoned caller is worth a recovery callback only if all three of these hold:

  1. The calling number is a real, dialable person — not a withheld number, not a forwarding switch, not a malformed entry.
  2. They haven’t already been helped since they abandoned — a caller who rang back twenty minutes later and got through doesn’t need a recovery call.
  3. Their history with this business shows productive engagement, not noise — a pattern of real interaction, not just a pattern of hanging up.

The rest of this system is nothing more than turning those three sentences into a repeatable filter.


6. The four-step funnel

Here’s the shape of the whole thing before the detail. Every abandoned pair passes through four gates in order — call-forwarding check, dialability check, already-connected check, and finally a score — and each pair is excluded at exactly one gate or survives to the worklist. Because each pair can only be excluded once, the counts at every stage add up exactly; nothing is double-subtracted.

flowchart TD
    A["~30,000 abandoned calls"] --> S1{"Step 1<br/>Call-forwarding line?"}
    S1 -->|yes · ~350| X1["Not the caller —<br/>a forwarding switch"]
    S1 -->|no| S2{"Step 2<br/>Dialable number?"}
    S2 -->|no · ~100| X2["anonymous / restricted /<br/>unavailable / unknown"]
    S2 -->|yes| S3{"Step 3<br/>Connected since<br/>the last abandon?"}
    S3 -->|yes · ~4,700| X3["Already helped —<br/>no recovery needed"]
    S3 -->|no| S4{"Step 4<br/>goodMsg &gt; ccCount?"}
    S4 -->|no| X4["Not recoverable —<br/>more concluded calls<br/>than productive ones"]
    S4 -->|yes| R(["A few hundred callers<br/>worth calling back"])

(All figures on this diagram are illustrative, rounded from a real analysis window — not exact production numbers.)

Step 1 — Exclude call-forwarding lines

Some client accounts forward their published number to us. When that happens, the calling number our system sees is the forwarding carrier’s switch, not the person who actually dialed the client’s number — and calling that switch back reaches nobody real.

Each account can list the numbers known to forward into it, and we check every abandoned caller against that list before doing anything else with it. This one caught a genuine mistake early on: our top target line by abandon volume looked, at first pass, like it had dozens of distinct “recoverable” callers. Every single one of them turned out to be the same forwarding switch reaching us dozens of different ways — not dozens of customers, one line. Adding this check dropped that line’s count to zero and pulled it off the target list entirely. Before the fix, our estimate of the real opportunity was overstated by roughly a fifth.

One caveat we build in and report explicitly: if the forwarding-line lookup fails for some reason, we flag that run as unverified, never as clean. A confirmed “zero forwarding lines found” and an unconfirmed “we couldn’t check” must never look the same in a report — the first is good news, the second is a gap.

Step 2 — Exclude non-dialable numbers

Not every abandoned call arrives with a number that can be dialed back. Carriers hand us placeholder values — anonymous, restricted, unavailable, unknown — alongside occasional malformed entries that aren’t valid phone numbers at all. None of these can ever be a callback target, so they’re dropped before any scoring effort is spent on them. The volume here is small — a few dozen to a hundred, depending on the window — but it’s free to check and it keeps the worklist from shipping rows an agent literally cannot act on. The test is simply whether the calling number is a well-formed, dialable phone number.

Step 3 — Exclude callers who already got through

This is the biggest cut in the whole funnel, and the least obvious one if you haven’t built a system like this before.

For every surviving pair, we track two timestamps: the most recent abandon, and the most recent time the caller actually connected — meaning an agent was attached to the call and real talk time was logged, not just a ring. If the connection is more recent than the abandon, the story is simple: this person called back on their own and got helped. There is nothing left to recover.

In our data, this single check removes the largest share of candidates — roughly 4,700 pairs in a typical window, more than any other step. It only works because we score every inbound call, not just the abandoned ones — the “they got helped” signal lives entirely in the calls that were answered, so if we only looked at abandons, we’d never see it.

Step 4 — Score what’s left on its own history

Everything that survives the first three gates gets one more test: does this caller’s history look like an engaged relationship, or does it look like noise? For each remaining pair, over the analysis window, we count two things from every logged call:

  • goodMsg — the number of calls where a productive message outcome was recorded: a message was saved, sent, taken, or the call was answered. These are the outcomes where the caller reached someone and something useful happened.
  • ccCount — the number of calls where the agent recorded a call conclusion. A call conclusion means the agent formally closed the call out — hung up, no response, wrong number, solicitor, fax tone, recorded message, and similar dispositions. A recorded conclusion is not a good sign here; it means the call ended without turning into anything productive. Every conclusion type counts toward ccCount equally, regardless of which one it is.

The rule:

recover = goodMsg > ccCount

Read in plain English: this caller has more productive outcomes on record than concluded, going-nowhere calls, so the relationship is live and a callback has something to build on.

Two things we learned tuning this:

  • Recorded conclusions are extremely common, and they cluster around hang-ups — if we’d counted “any recorded activity at all” as a positive signal, almost every caller would have scored recoverable, which makes the score meaningless. Restricting the positive side to those four specific outcomes is what gives the rule any discriminating power at all.
  • Some conclusion types flag the caller as low-value on their face — solicitor, spam recording, fax tone, wrong number, recorded message. We count these and surface them, but we don’t auto-exclude on them. The rule treats every conclusion equally, by design, so a caller with a solicitor flag can still score as recoverable if their other history supports it. We’d rather show a human that flag and let them make the call than quietly override the rule ourselves. The same goes for any conclusion type our reference list doesn’t recognize — it gets labeled as unknown and still counted, rather than silently dropped, so the totals a reader sees always add up.

Put the whole funnel together and the shape is roughly this, in illustrative round numbers:

StageRemainingRemoved at this stepWhy
Abandoned calls in the window~30,000
After excluding forwarding lines~29,650~350Forwarding switch, not a real customer
After excluding non-dialable numbers~29,550~100anonymous / restricted / unavailable / unknown
After excluding already-connected callers~24,850~4,700Got through on their own since abandoning
After scoringa few hundredmost of the remaindergoodMsg did not exceed ccCount

Roughly one in a hundred abandoned calls ends up on the worklist. That’s not a disappointing number — it’s the point. The other ninety-nine were never going to be worth an agent’s time, and now we know which ones actually are.


7. How it runs, end to end

The pipeline behind all of this is intentionally simple. Amazon Connect streams Contact Trace Records to Kinesis; a processing function normalizes each record and writes it to our contact-record store. A recovery analyzer then runs over a batch of business lines at a time, pulling each line’s abandoned callers, running the four-step funnel against each one, and producing a scored worklist. It’s built to be resumable — if a batch fails partway through, a local driver checkpoints progress and picks up where it left off rather than starting over, and the analyzer itself watches its own time budget so a slow run returns a partial, resumable result instead of failing outright.

Stripped of any internal naming, the guard chain at the center of it reads like this:

for each abandoned (line, caller) pair:
  if caller is a known forwarding line for this account:      exclude  // forwarding switch
  if caller's number is not a well-formed, dialable number:    exclude  // can't be dialed
  if caller connected (with real talk time) after last abandon: exclude  // already helped
  score = goodMessageCount > callConclusionCount
  if score: add to worklist

That’s the whole decision. Everything else in this post is context for why each line is there.


8. Acting on the list: setting up outreach in Amazon Connect

A worklist nobody calls is just a report with extra steps. This section is about what happens once we have those few hundred names: what does dialing them back actually look like inside Amazon Connect?

8.1 Four ways to reach out, and when each one fits

Amazon Connect gives you more than one way to act on a list like this, and the right choice depends on the list’s size and how much judgment each row deserves:

ApproachHow it works in ConnectFits when
Agent-reviewed taskCreate a task contact per recoverable caller, routed to a recovery queue with the reason attached; an agent reviews it and dialsA shorter, high-value list where a human glance before dialing is worth the time
Direct outbound callStart an outbound voice contact per caller; Connect places the call, runs an outbound flow, and connects an agentSteady, moderate volume where you want per-call control
Outbound campaignAmazon Connect Outbound Campaigns, which supports voice, SMS, and email with a predictive dialer and machine-learning-based answering-machine detectionLarger lists where campaign pacing matters more than per-row judgment
Text firstThe SMS channel inside Outbound CampaignsThe cheapest, least intrusive first touch — escalate to voice only if they respond

Our own choice, and this is a recommendation rather than a hard rule: task-first. Our worklist runs a few hundred rows, and every row carries a specific reason it made the list. That’s exactly the kind of list worth a human’s eyes before a phone rings — auto-dialing the raw output of a scoring rule at volume is precisely where annoyance and compliance risk creep in, and our list is small enough that the heavier campaign machinery buys us little.

8.2 The dispatch flow

Getting from “this pair is on the worklist” to “an agent is talking to them” runs through one more layer — a dispatcher that re-checks the pair right before anything happens, then hands it to Connect:

flowchart LR
    W[("Recovery worklist")] --> T["Scheduled trigger"]
    T --> D{"Dispatcher checks"}
    D -->|quiet hours in<br/>account timezone| H["Hold for<br/>next window"]
    D -->|suppressed / opted out| S["Drop"]
    D -->|connected since<br/>list was built| C["Drop —<br/>already helped"]
    D -->|clear to contact| K["Create contact<br/>in Amazon Connect"]
    K --> F["Outbound flow:<br/>attach context attributes"]
    F --> Q["Recovery queue"]
    Q --> AG["Agent — whisper gives<br/>line, wait time, attempts"]
    AG --> O["Outcome recorded"]
    O -.->|new contact records<br/>re-enter the pipeline| W

A few points worth pulling out of that diagram.

Re-run the “already connected” check right before dialing, not just when the list was built. A worklist is a snapshot. Time passes between generating it and acting on it, and in that gap, a caller can ring back and get helped on their own — which is exactly the condition step 3 of the funnel already knows how to test. Running that same check again immediately before dialing is nearly free, and it prevents the single worst outcome in this whole system: an agent opening a call with “I’m following up on your missed call” to someone we already spoke to an hour earlier.

Give the agent context, not just a number. The outbound flow attaches useful call metadata to the contact — which line was called, when the abandon happened, how long they waited, how many times they’ve tried — and whispers it to the agent before the caller connects. “You called us on Tuesday and waited a couple of minutes” is a very different opening line than a cold dial, and it costs nothing extra to build: it’s all metadata the system already holds, with no need for any personal detail about the caller beyond the call history itself.

Guardrails belong in the dispatcher, not in a document. Quiet hours evaluated against the account’s own timezone, suppression and opt-out lists checked at the moment of dialing, regulated or sensitive accounts excluded from outreach entirely, and concurrency held under sensible limits so pacing is enforced by the system rather than by policy. Anything that only lives in a runbook gets skipped the first time someone exports the list to a spreadsheet and works it by hand — building the rule into the dispatcher is what actually makes it durable.

Close the loop. Outbound contacts generate their own Contact Trace Records, which flow through the exact same stream-and-store pipeline that produced the worklist in the first place. Tag those outbound contacts at dial time, and every recovery attempt becomes measurable inside the same system that recommended it — contacted, connected, productive outcome. That measurement is what eventually turns the scoring rule in step 4 from a reasoned heuristic into something calibrated against real outcomes.


9. How other contact centers approach abandoned-call recovery

Most of the industry’s attention on abandoned calls goes toward preventing them rather than recovering after the fact, and it’s worth being fair about that approach’s real strengths before contrasting it with ours.

  • Virtual queue / queue callback. A caller can hang up, keep their place in line, and get an automatic callback from an agent when one becomes free — often timed to off-peak windows to smooth out load. This is prevention, not recovery: it stops the abandon from happening in the first place rather than acting on one after the fact.
  • Missed-call text-back. The moment a call goes unanswered or is abandoned, an automatic text goes out acknowledging the call and setting an expectation for a follow-up. This is recovery, but undifferentiated — it typically reaches everyone who abandoned, within minutes, with no attempt to separate a real prospect from a wrong number or a forwarding line.
  • Dialer list export. Abandoned numbers get exported into a general outbound calling list or campaign and worked like any other lead list.
  • IVR and staffing fixes. Treat the abandon rate purely as an operational metric — shorten menus, add capacity, and reduce the rate at the source rather than acting on individuals who already abandoned.

The strongest of these — automatic post-abandon text-back — has a real advantage we don’t match: speed. It reaches someone within seconds of the hang-up, while our worklist runs on a scheduled, batched cadence. For a single business with modest call volume, blasting a text to everyone who abandons is a perfectly reasonable policy, and it’s simpler to build than anything in this post.


10. What’s different about ours — and did we actually find the worthy callers?

Typical blast approachOurs
Unit of analysisThe queue or the dayThe specific caller-and-line pair
Forwarding linesNot distinguished from real callersIdentified and excluded before scoring
Already-resolved callersNot checkedExplicitly excluded — the single largest cut
Engagement historyNot consideredScored from the caller’s own message and conclusion history
OutputA report, or an undifferentiated contact listA ranked worklist, with a reason per entry

So, did the funnel actually find the right people? The honest answer has two halves.

Yes, in the sense that matters for defensibility: the list is clean by construction. Every excluded class has a stated reason and a stated count. Roughly 4,700 people we would otherwise have interrupted with an unnecessary callback are correctly off the list. And the forwarding-line fix caught a real, verified error — one that would have sent agents to dial the same switchboard dozens of times over, believing each attempt was a different customer.

Not yet proven, in the sense that would make this a model rather than a heuristic: we haven’t measured the actual reconnect or booking rate of “recoverable” callers against “not recoverable” ones. Until that comparison runs over a few weeks of real outreach, the goodMsg > ccCount threshold is a reasoned rule, not a validated one. Section 8’s “close the loop” step exists specifically to generate the data that answers this question honestly, rather than assuming the answer.

Worth naming a couple of open caveats plainly, rather than glossing over them: the forwarding-line exclusion is only as good as how current that list is kept, some call conclusion types show up that our reference data doesn’t recognize, and a caller flagged with a “junk” conclusion (spam, fax tone, solicitor) can still score as recoverable by the rule as written — we surface that flag for a human rather than silently overriding the score.


11. From recovery list to lead generation for our clients

Mechanically, this is already covered — the dispatch flow in section 8 is exactly what runs here too. What’s different is the framing: for our clients, a recovered caller isn’t a support ticket, it’s a lead.

  • Prioritize by business value, not just volume. Lines that have several recoverable callers get worked first, then the individual callers with the most abandons on record. A handful of lines typically carry a disproportionate share of the opportunity, so this ordering matters more than treating every row equally.
  • What the client actually sees. Rather than a raw abandon report — the kind of document that gets glanced at once and never opened again — the deliverable is closer to a monthly “here are the customers we recovered for you” summary, with the reason each one was contacted.
  • The exclusions are the pitch, not a footnote. “We contacted 300 people” is a weaker claim on its own than “we contacted 300 people, and we deliberately did not contact 4,700 who had already been helped.” The discipline of the funnel is what makes the number trustworthy.
  • Where this works hardest. The value of a single recovered caller scales with how much one missed engagement is worth to the client — legal, medical, and trades/home-services businesses see the clearest return. For high-volume, low-value inbound, the economics are weaker, and it’s worth saying that plainly rather than overselling the approach everywhere.
  • The metric that closes the argument. The same contacted → connected → productive-outcome measurement from section 8 becomes the number that proves the value of this program to a client, month over month — not a story about effort, but a count of outcomes.

12. The uncomfortable footnote, and what’s next

Here’s a finding from this same work that recovery does not fix: roughly 94% of abandoned calls happen in under fifteen seconds, on lines that typically start answering in a matter of seconds. That’s not a queue-capacity problem. That’s a greeting-and-IVR-length problem — people are hanging up before the call flow even finishes introducing itself.

Recovery treats the symptom, not the cause. It’s a genuinely useful program on its own terms, but if a line’s greeting gets shorter and its early call flow gets tighter, its recovery worklist should shrink on its own. If it doesn’t, that’s a sign something else is still wrong upstream.

What’s next for this system: moving the analysis to a regular scheduled cadence rather than ad-hoc runs, writing results to durable storage instead of local files, adding an explicit suppression list for opt-outs and sensitive accounts, and — most importantly — running the outcome measurement described in sections 8 and 10 long enough to turn the scoring rule from a reasoned heuristic into something calibrated against what actually happens when we call these people back.


Sources on Amazon Connect’s outbound capabilities, referenced in section 8:

Sources on general industry abandoned-call practice, referenced in section 9: