Connect by JBRH Open Connect

When a call is answered in silence

A call that connects and then plays nothing leaves the same trace as a caller who hung up immediately. On 2026-09-07 five inbound calls sat silent and were recorded as abandonment. The cause was a greeting the process could not synthesise. The call now ends deliberately, posts engine_error {at: greeting}, and is written down as something that was never a conversation.

Status
Available What this means
Audience
both, developer
Channels
phone
In the app
#/calls, #/needs-you
Last verified
Product version
6.3.2

Two very different calls, one indistinguishable record#

A caller who rings, hears nothing and hangs up after four seconds produces a short answered call with no caller speech. A caller who rings, is answered by a process that cannot speak, and hangs up after four seconds produces exactly the same thing. One is ordinary; the other is a total failure of the product, and for a while both were summarised as *the caller hung up before speaking*.

A failure that files itself under a benign heading is worse than a loud one. Nobody investigates a hang-up. The five calls were only found because a restart pattern was being looked at for another reason.

What was actually happening#

The SDK runs each call in its own process and calls the prewarm hook in every idle process. Each of those processes asked the text-to-speech model for the same greeting sentence, under the same cache key, in the same millisecond. The model's per-minute quota answered one of them 429 — and a refused synthesis was treated as a permanent answer, so that process had no greeting for the rest of its life. Whichever call landed on it was answered in silence.

A second, quieter contributor sat underneath: process initialisation had a default bound of 10 s, which was *shorter* than the warm-up's own budget. The warm-up could be killed for being slow before it had a chance to fail for being refused.

The fix has two halves#

  1. Stop the stampede. Warm-up now spreads processes over WARM_STAGGER_S and retries a refused synthesis WARM_RETRIES times with backoff when the refusal is one that passes — 429 and 5xx, never a bad key.

    Result refused is counted separately from failed, so a quota problem and a broken credential no longer look alike. The worker asks for initialize_process_timeout 45 s against a WARM_TIMEOUT_S of 30 s.

  2. Make an unsayable greeting end the call rather than start one. _say asks once more when the refusal passes; if the line still cannot be spoken on a model that takes no generated line, the worker posts engine_error {at: greeting} and ends with hangup_by=engine.

    Result The engine writes facts.silent_failure from that event, voice.no_conversation says so, and the outcome is not_reached — a call that never rang a conversation into existence.

The warm-up itself is the reason the greeting is worth this much trouble: a cached line starts in 0 ms against 3.0 s for live synthesis, and greeting.prewarmed on the event says which of the two the caller got.

The vocabulary that keeps the cases apart#

no_answer
It rang and nobody picked up. An ordinary outcome.
not_reached
It never rang — the dial, the dispatch or the worker failed. Not the caller's doing.
silent_failure
It was answered and nothing could be said. Written from the engine_error event, not inferred.
line_off / after_hours
A deliberate refusal: the closed-line message was spoken in the business's own name, then the call ended.
hangup_by
One of caller, model, owner, engine, carrier, agent. Before this, every ending that was not the caller's read *agent*.

_capture_lead creates no lead from a call that was never a conversation, which stops a run of failures from quietly manufacturing prospects. The refusal path is equally deliberate: a switched-off line used to be a bare 409 that the worker answered by deleting the room, so the caller heard the call connect and drop and no Call row existed at all. It now returns a refusal brief carrying closed_line_text — deterministic, warmed alongside the greetings, and with no opening hours in it.

What is still only partly covered#

  • voice_engine.line_health catches the adjacent failure — a worker that has not checked in, so an inbound call reaches nothing at all — which the Phone screen once reported as *ready*. It drains by itself when the cause clears.
  • Silence with a cause outside the process, such as a one-way audio path on the carrier leg, is not covered by this event and would still read as a short call.
  • The five calls are the only counted instance. No ongoing rate of silent answers is measured here: UNKNOWN.
  • The guard depends on the greeting being a known utterance. A model that could generate its own opening would need a different signal that speech began.

Questions#

Why end the call instead of continuing without a greeting?

Because the caller has no way to know a conversation started. Continuing produces a call where the first thing a stranger hears is silence, then possibly a reply to something they said into that silence. Ending immediately with the failure on the record is honest, and it makes the fault countable.

How would I notice this on my own line?

Compare the carrier's own call log against the call records. A completed, charged call on their side with nothing or a four-second nothing on yours is the signature. That comparison is the fastest audit available on any telephony integration and it has found more than one defect here.

Does a refused greeting count against the caller?

No. The outcome is not_reached, no lead is captured, and the ending is attributed to the engine rather than to the caller — which is the whole point of hangup_by having six possible values instead of defaulting to one.