Greeting warm-up
Every idle voice worker process synthesises the greetings a workspace might need before any call arrives and keeps them in that process's own cache. When somebody rings, the opening line is already audio: the wait went from about three seconds to none. A miss is not a failure — the sentence is synthesised live instead, a little slower.
Why the first sentence is the expensive one#
On the realtime engine the greeting is not generated by the live model. Gemini Live refuses say(), so the opening line is made by Gemini TTS and played with say(audio=…) the instant the caller picks up. That is the right design for control — the sentence is deterministic, it is the business's own words, and it is spoken with the caller's audio muted so nobody talks over it — but it puts a text-to-speech round trip on the critical path of every call.
Warming it moves that round trip off the call entirely. The SDK runs each call in its own process and calls the prewarm hook in every idle process, so agent.prewarm asks the engine for the greetings this worker might need (GET /api/voice/engine/greetings) and synthesises each one into that process's LineCache.
| Key part | Changes the audio because |
|---|---|
| Workspace | Two businesses saying the same sentence are still two recordings |
| TTS model | gemini-tts and chirp3-hd do not sound alike |
| Voice | The named voice is the identity of the line |
| Style | The natural-language style instruction is part of the request |
| Language | The same greeting in another language is another file |
| Text | The obvious one: a changed greeting must miss the old entry |
voice_style.greeting_key builds that key, and the closed-line message is warmed alongside the greetings under kind: closed — a refused call should not be the slow one.
The morning every restart warmed nothing#
Until 2026-09-07 the warm-up looked like it worked and did not. Each idle process started at the same moment, asked the text-to-speech API for the same sentence under the same key in the same millisecond, and the model's per-minute quota answered one of them 429. The hook swallowed the refusal, the process kept running, and that process never had a warm greeting again for the whole of its life.
Three changes fixed it, and each one addresses a different part of the failure rather than the symptom:
- Processes are spread across
WARM_STAGGER_Sinstead of starting together, so the same key is not requested many times at once. - A refused synthesis is retried
WARM_RETRIEStimes with backoff when the refusal is one that passes — a 429 or a 5xx. A bad key is never retried, because waiting will not fix it. refusedis counted separately fromfailed. A quota refusal and a broken request are different operational problems and used to look identical.
There was a fourth thing wrong underneath. The warm-up runs inside process initialisation, and the SDK's default bound on that (10 s) was shorter than the warm-up's own budget — so on a slow morning the framework killed the work before it could finish. The worker now asks for initialize_process_timeout of 45 s and keeps WARM_TIMEOUT_S at 30 s, in that order, so the inner budget expires first and reports.
What you can see#
greeting.prewarmed- On the call's event record: whether this greeting came from the cache or was synthesised while the caller waited.
- Inbound greeting time
- About 3.0 s before warming, 0 ms on a hit.
refused/failedcounts- Quota refusals against real errors, per warm-up run.
A greeting that cannot be spoken at all is a different matter and is not handled here. _say asks the text-to-speech API once more when the refusal passes; if the line still cannot be said on a model that will not generate one for itself, the worker posts engine_error {at: greeting} and ends the call rather than leaving somebody listening to nothing. Calls that were never a conversation covers what that leaves on the record.
Questions#
If I change the greeting, does the next call use the old audio?
No. The greeting text is part of the cache key, so a changed sentence cannot match a warmed entry — it misses, is synthesised live on that call, and is warmed into new processes from then on. The cost of an edit is roughly one slower greeting per running process, not a stale one.
Does warming cost anything if nobody calls?
It costs the text-to-speech synthesis of a short sentence per idle process, per greeting the workspace has. It is not model-usage on a call and does not appear against a call's cost, because there is no call. It is why the warm list is the greetings and the closed-line message rather than anything longer.
Why not warm the whole first reply as well?
The first reply is generated by the live model in response to whatever the caller said, so there is nothing to warm — it is not known in advance. The greeting is the only sentence on a call whose exact wording is decided before the call exists.