Start with the call in Call History: its disconnection_reason, its transcript, and when it ended. Then match the symptom below.
Disconnection reasons#
| Reason | Meaning | Check |
|---|---|---|
error_llm_websocket_open |
The worker could not open {llm_websocket_url}/{call_id} at the start of the call, or the agent has no URL. |
URL scheme (ws:// or wss://), DNS, TLS certificate, firewall, that your server accepts any call id as the last path segment. Run Test connection. |
error_llm_websocket_lost_connection |
The socket closed and could not be restored: no auto_reconnect, a reconnect failed, or 2 reconnects were already used. |
Server restarts and deploys, load balancer idle timeouts, ping handling, auto_reconnect in config. |
error_llm_websocket_corrupt_payload |
A frame was not valid JSON, had an unknown response_type, or lacked a required field. |
Frame shapes (see WebSocket server). A common cause is a missing response_id. |
error_llm_websocket_runtime |
The worker's reader failed, or no frame of a requested response arrived for 30 seconds. | Slow model or tool calls without intermediate chunks, an exception that stopped your send loop. |
The call connects but the agent never speaks#
- Nobody speaks first. A custom LLM agent only opens the call if your server sends
responsewithresponse_id: 0before the first request. Otherwise the agent waits for the caller. - The opening line arrived late. A
response_id: 0frame that arrives after the firstresponse_requiredis ignored. - No
content_complete: true. Opening lines andagent_interrupttext are buffered until the last chunk. Without it nothing is spoken. - Wrong
response_id. Frames must carry the id of the latest request. Older ids are dropped silently.
The agent cuts off or replies to the wrong turn#
- Your server answered an old
response_idafter the caller interrupted. Cancel work when a newer request arrives. - Your server built its reply from
update_onlyframes, which can arrive just after the request. Use the transcript inresponse_required.
call_details never arrives#
The worker sends call_details only if your config frame with call_details: true arrives within 0.5 seconds of the socket opening. Send config as the first frame on open.
Test connection sends a call_details frame without waiting for config. A server can receive call_details during the test and still miss it on real calls if config is slow.
Pings stop or calls reconnect mid-turn#
- Pings only start if
auto_reconnect: truewas in aconfigreceived within 0.5 seconds of connecting. - If your server handles
ping_pongbehind a model call, replies arrive late and the worker reconnects after 5 seconds. Answer pings on their own path. - Reverse proxies with short idle timeouts can close quiet sockets. Allow at least the call's maximum duration.
Actions do not happen#
| Symptom | Cause |
|---|---|
end_call ignored |
Set on a frame for an old response_id, which was dropped. Or transfer_number was also set; transfer wins. |
| Transfer fails | Web calls cannot be transferred. On phone calls, check the number is E.164 and the trunk allows outbound calls. |
| Digits not sent | digit_to_press needs a phone call. Digits are sent after the speech finishes. |
update_agent has no effect |
Only responsiveness changes live behaviour in the current worker. interruption_sensitivity, reminder_trigger_ms and reminder_max_count are stored but read at call start. |
metadata not on the call |
metadata frames are accepted but not stored. |
No transcript_with_tool_calls in requests |
The worker sends transcript only, even when config.transcript_with_tool_calls is true. |
Reminders never arrive#
reminder_required needs reminder_max_count above 0 on the agent (default 1) and reminder_trigger_ms of silence (default 10000). Silence is measured while neither side is speaking. If end_call_after_silence_ms is shorter, the call ends with inactivity first.
Test connection fails but calls work (or the reverse)#
Test connection runs from the API and refuses private, loopback and reserved addresses unless EGRESS_ALLOW_PRIVATE=1 is set on the API. The voice worker connects from wherever it runs. A URL on your local network can pass on calls from a local worker and fail the test, and a firewall that allows only the worker's address can fail the test and pass on calls.
Responses are slow#
- Measure your time to first chunk per
response_id. - Compare
latency.llm_websocket_network_rtton the call with your own logs. A high round trip means distance; a low round trip with slow turns means your server. - See Best practices.
Still stuck#
Reproduce the call with the text simulator and --events -. It uses the same engine as a live call and prints every event, including the final call_ended reason.