clever-sunset-98387
02/25/2026, 12:34 PMtall-airline-16756
02/25/2026, 12:34 PMtall-airline-16756
02/25/2026, 12:34 PMlivekit/plugins/sarvam/tts.py when await ws.send_str(...) runs and aiohttp raises ClientConnectionResetError: Cannot write to closing transport.
• The failures appear during concurrent agent sessions and the plugin retries, e.g. "failed to synthesize speech: Connection error., retrying in 0.1s".
• This looks like a race condition or lifecycle/connection-management issue where a WebSocket is being used while it is closing.
What you can try next (ordered by easiest first)
1. Add defensive checks and catch the specific websocket/send error
◦ Before sending, check connection state if available and catch aiohttp.client_exceptions.ClientConnectionResetError and ConnectionResetError to retry or recreate the WS.
◦ Example pattern:
i. Try send
ii. On ClientConnectionResetError / ConnectionError, close and recreate the websocket and retry with backoff
◦ This prevents one send failure from crashing the task and provides a graceful reconnect.
1. Ensure each concurrent agent session uses an isolated TTS websocket instance
◦ Confirm your code does not share a single ws or TTS client instance between sessions; each session should create/own its TTS websocket.
◦ Shared instances cause one session’s close to affect others, producing exactly this symptom.
1. Improve retry/backoff with jitter
◦ Instead of immediate repeated retries, use short exponential backoff with jitter (e.g., 0.1s -> 0.3s -> 1s) so bursts of concurrent retries don’t overload the Sarvam endpoint.
◦ Limit retries to a small number and surface clear errors after failure.
1. Delay cleanup until audio sending is confirmed complete
◦ Make sure session shutdown/cleanup waits until all TTS sends and playback are finished. If cleanup triggers while sends are in-flight, you will see "closing transport" errors.
1. Add lifecycle logging around the websocket
◦ Log open, close, send attempts, exceptions, and the stack trace to identify whether closures come from your side, Sarvam, or network problems.
◦ This helps tell whether connections are being closed intentionally (cleanup) or unexpectedly (remote close or network).
1. Confirm Sarvam concurrent connection limits and health
◦ Check whether the Sarvam service imposes per-account/per-IP connection limits or aggressively closes connections under load. If it does, reduce concurrent websocket count or pool requests appropriately.
1. Verify dependency versions
◦ Ensure you are on modern versions of aiohttp, livekit, and the Sarvam plugin; connection handling bugs have been fixed in some aiohttp versions.
Targeted code ideas
• Wrap sends in a helper that checks and recreates the websocket on failure, e.g.:
async def safe_send(ws, payload, recreate_ws_fn):
try:
await ws.send_str(payload)
except (aiohttp.client_exceptions.ClientConnectionResetError, ConnectionError) as e:
# close current if needed, recreate, then retry once
try:
await ws.close()
except Exception:
pass
ws = await recreate_ws_fn()
await ws.send_str(payload)
return ws
• Use small exponential backoff with jitter for retries.
Diagnostics I’d ask you to run (so I can help further)
• Do concurrent calls each create their own TTS websocket, or do they share one?
• How many concurrent TTS sessions are typical when you see the failure?
• Which versions of aiohttp, livekit, and the Sarvam plugin are you running?
• Do logs show the websocket was closed remotely (a close frame) or do they show local cleanup immediately before the send?
• Can you reproduce reliably with a small test script that opens N concurrent sessions and performs TTS? If so, what N?
If you share short answers to the diagnostics above and a minimal snippet showing how you create/manage the TTS websocket per session, I can suggest a concrete patch to livekit/plugins/sarvam/tts.py or a safe wrapper you can add to your agent to eliminate the race.
If you prefer, I can also provide a more specific retry/backoff implementation or a suggested lifecycle sequence (open → send config → stream → finalize → close) you can adopt. Which would you like next?
Sources: Sarvam TTS plugin guide | LiveKit Documentation | How to build an agent with speech-to-text input and text-only output | Live... | Server options | LiveKit Documentationclever-sunset-98387
02/25/2026, 12:54 PM@server.rtc_session(agent_name="AlgoflowSurvey")
async def my_agent(ctx: JobContext):
tts = create_sarvam_tts() # fresh instance per session
session = AgentSession(
stt=stt,
llm=llm,
tts=tts,
vad=vad,
)
So the shared instance theory does not apply here.
This happens within a single call:
This is not a concurrency issue. The error occurs mid-conversation in a single active session, between two consecutive TTS synthesis requests (between agent turns).
Key observations from logs:
1. server_request_id: null on the failed session — meaning Sarvam's server closed the WebSocket before acknowledging the request. The close is originating from the Sarvam side, not ours.
2. Same session_id: 138374040272048 across all failed log lines — confirming this is within a single session, not across concurrent ones.
3. Both send_task and recv_task fail simultaneously — the transport was already closing when the plugin attempted to write. The plugin has no prior warning.
4. The retry succeeds in ~0.5s with a fresh connection (client_request_id changes from 12840c7fe812 to 407335089cf3) — but during that 0.5s window the agent goes completely silent, causing a bad user experience.
5. The error sequence is:
ClientConnectionResetError: Cannot write to closing transport
failed to synthesize speech: Connection error., retrying in 0.1s
Fresh connection → WebSocket session completed successfully
Does Sarvam's WebSocket server have an *idle timeout* between messages? If so, what is it? This would explain why the connection is closing between agent turns.
Can the plugin implement a *proactive connection health check* — detecting remote close frames before attempting to send — so it reconnects before the next synthesis request rather than after it fails?
Can the *retry delay be reduced or made configurable* below 0.1s to minimize the silence gap when this does happen?
Is there a *keep-alive or ping mechanism* available in the plugin to prevent Sarvam's server from closing idle connections between turns?
1. Is this a *known issue* in the current plugin version, and is there a fix planned or a workaround recommended for production voice agents?
Environment:
Plugin: livekit-plugins-sarvam (latest)
Model: bulbul:v2
• Speaker: anushka
• Use case: IVR/voice survey agent with multiple turns per calltall-airline-16756
02/25/2026, 12:55 PMsession_id throughout shows this is mid-session, not cross-session interference.
• The plugin detects the closed transport only when attempting to write (both send_task and recv_task fail together).
• A fresh connection succeeds quickly, but the reconnect causes noticeable silence for callers.
What the Sarvam TTS plugin currently does (and does not do)
• The plugin uses a WebSocket streaming connection for TTS.
• The plugin detects connection failure reactively (on send/receive failure) and then reconnects; your logs show the retry path that opens a new client_request_id.
• There is no documented or exposed keep-alive/ping/heartbeat in the plugin to prevent idle timeouts.
• There is no documented configurable pre-send health check or configurable retry delay exposed by the plugin (the 0.1s retry appears internal and not user-configurable).
• Because the plugin lacks proactive health checks or keep-alive, the client only knows the transport is closed when it tries to write — which creates the silent gap.
Likely root cause
• Sarvam is very likely closing idle WebSocket connections (an idle timeout). That explains a remote close between turns, the null server_request_id, and why a fresh connection fixes it immediately.
Practical mitigations you can apply now 1) Pre-warm the connection immediately before each turn
• Send a very small/no-op or silent synthesis before the real TTS so you detect/recreate a closed WS and avoid the visible gap:
◦ Example pattern: try a tiny synth (a short silence or punctuation), then the real text.
• This trades a negligible extra request for consistent low latency.
2) Implement a lightweight “idle-aware” wrapper around the plugin
• Track last-used timestamp; if idle > threshold (e.g., 5–8s), proactively reinitialize the TTS connection before sending the next utterance.
• This prevents the client from trying to write to a transport that Sarvam already closed.
3) Add pre-send safe-send logic with immediate reconnect
• Attempt send; if it raises aiohttp.client_exceptions.ClientConnectionResetError (or ConnectionError), close and recreate the websocket and retry once immediately (no long backoff).
• Keep this retry quick (retry once immediately, then a short backoff) so you minimize the silence window.
4) Backoff/jitter tuning (if you modify plugin)
• If you control the retry code, use a two-step retry: immediate quick retry (reconnect) then a short backoff with jitter for subsequent retries. This reduces user-visible silence while avoiding thundering reconnect bursts.
5) Mask reconnection with fallback audio
• If you can’t completely avoid the gap, play a short neutral prompt or tone while reconnecting so the caller doesn’t experience awkward silence.
6) Measure the idle timeout
• Run a small test that opens a WS session, waits N seconds, then attempts a send. Increase N to find the idle timeout threshold. This helps tune your pre-warm/idle threshold.
Longer-term fixes (recommended)
• Add keep-alive/ping logic to the Sarvam plugin (or fork and add it):
◦ Periodic WebSocket pings or lightweight heartbeats during inter-turn silence would prevent Sarvam from closing the connection.
• Expose configurable retry/backoff and an idle timeout guard in the plugin:
◦ Make the retry delay configurable and allow pre-send health checks.
• Confirm with Sarvam what their idle timeout policy is and whether they recommend a recommended keep-alive interval.
Suggested next steps I can help with
• Provide a small safe-send wrapper you can drop into your agent to detect closed transports, reconnect, and retry with minimal delay.
• Help draft a short test script to measure Sarvam’s idle timeout in your environment.
• Help craft an issue / PR for the plugin to add keep-alive and configurable retry/idle parameters if you’d like to contribute a fix.
Which would you like first: (A) a compact safe-send wrapper you can integrate now, or (B) a small idle-timeout measurement script to determine the timeout value?
Sources: Sarvam TTS plugin guide | LiveKit Documentation | Agents Framework | livekit/livekit_composite | DeepWiki | Agents (Python) | livekit/livekit_composite | DeepWiki | examples/voice_agents | livekit/livekit_composite | DeepWikiclever-sunset-98387
02/25/2026, 1:11 PMtall-airline-16756
02/25/2026, 1:12 PMping (or a small JSON heartbeat if server requires application-level) every configurable interval (e.g., 5s).
◦ Reset last-pong/last-activity timestamp on any pong/recv.
◦ If pong not received after N intervals (configurable), mark the connection unhealthy and initiate reconnect logic.
• Rationale: Prevents the server from classifying the socket as idle and closing it between turns.
2) Pre-send health check (fast path)
• Before each synth send:
◦ Check a small internal is_alive flag derived from the last pong/recv timestamp and WS state.
◦ If is_alive is false (or WS.closed), proactively recreate the WS synchronously before sending.
◦ If is_alive is true, proceed to send.
• Rationale: Avoid writing to a transport that is already closing—this prevents the ClientConnectionResetError.
3) Safe-send with immediate reconnect and bounded retries
• Implement a safe_send wrapper used for all outgoing messages:
◦ Try sending once.
◦ On aiohttp.client_exceptions.ClientConnectionResetError, ConnectionResetError, BrokenPipeError, or `ConnectionError`:
i. Immediately cancel pending send task(s).
ii. Close WS gracefully (ignore errors).
iii. Recreate WS synchronously (one immediate reconnect attempt).
iv. Retry send once.
◦ If still failing, perform 1–2 additional retries with a short exponential backoff + jitter (configurable; e.g., 50–200ms).
◦ Surface clear errors to the agent after exhausted retries.
• Rationale: Immediate reconnect minimizes user-perceived silence; bounded retries and backoff avoid thundering reconnects under load.
4) Configurable parameters (expose to integrators)
• Expose plugin config options with safe defaults:
◦ heartbeat_interval (default 5s)
◦ heartbeat_timeout / max_missed_heartbeats (default 2)
◦ pre_send_idle_threshold (e.g., 1.5x heartbeat_interval)
◦ reconnect_retries (default 3)
◦ reconnect_backoff_base_ms (default 50)
◦ reconnect_backoff_max_ms (default 300)
• Rationale: Different deployments (IVR vs. chatty agents) can tune for cost vs. latency.
5) Connection lifecycle and resource safety
• Pooling: keep per-session WS (as you already do). Do not share across sessions.
• Graceful shutdown: ensure background heartbeat and health tasks are cancelled on session close; cleanly close WS.
• Rate limiting: prevent excessive reconnect loops by capping reconnects per minute per session and emitting a metric/error when exceeded.
• Rationale: Avoid runaway resource use or DDOSing Sarvam under persistent failures.
6) Observability, metrics, and logging
• Emit structured logs / metrics for:
◦ heartbeats sent, pongs received, missed heartbeats
◦ proactive reconnects vs. reactive reconnects
◦ reconnect latency and retry counts
◦ final failures (so operators can detect systemic issues)
• Rationale: Allows ops to tune heartbeat interval and detect server-side timeouts or network instability.
7) Optional: Application-level “keepalive” if server needs it
• If Sarvam does not honor WS-level pings or drops pings, use a light application-level keepalive message that Sarvam will acknowledge.
• This requires agreement with Sarvam (or testing to ensure such messages are accepted).
Safety & correctness considerations
• Default heartbeat must be conservative (e.g., 5s) to minimize overhead and cost.
• Keep reconnect attempts bounded and add exponential backoff + jitter.
• Ensure all background tasks are cancelled on session end to avoid leaks.
• Add unit/integration tests simulating server close between turns and verifying no audible gap beyond target (e.g., <150ms for reconnect where possible).
Suggested minimal change set (concrete)
1. Add a Heartbeat task in Sarvam TTS client class:
◦ start: after ws open -> spawn heartbeat coroutine
◦ heartbeat coroutine:
▪︎ loop: await sleep(heartbeat_interval); send ping or heartbeat frame; wait for pong or timeout; if timeout increment missed; if missed >= limit -> mark unhealthy and call reconnect
1. Add safe_send wrapper used by send_task to pre-check is_alive and to handle immediate reconnect on write error.
2. Add plugin config options and environment-based defaults.
3. Add metrics/logging and unit tests.
4. Release patch, and document config options.
Why this is production-safe
• Proactive heartbeats keep connections alive across normal inter-turn silence.
• Pre-send checks prevent writing to a transport that is closing.
• Immediate reconnect minimizes user-perceived silence; bounded retries and backoff protect server and client resources.
• Tuning parameters let you find balance for IVR latency vs. connection cost.
If you’d like, I can:
• Provide a concrete patch outline (code sketch) for the Sarvam plugin showing:
◦ heartbeat implementation (aiohttp ping or app-level keepalive),
◦ safe_send wrapper,
◦ config plumbing, and
◦ test scenarios.
• Draft a PR description you can use when contributing this change to the plugin repo.
Which would you prefer next — a focused code sketch of the heartbeat + safe_send implementation, or a ready-to-submit PR description and test plan?