zeroclaw-labs/zeroclaw · error
Edge TTS subprocess timed out
Error message
Edge TTS subprocess timed out
What it means
EdgeTtsProvider::synthesize spawns the edge-tts CLI and waits on an absolute deadline (TTS_HTTP_TIMEOUT, 60s) shared by the process wait and the stderr drain. If child.wait() does not finish in time, the child is killed and reaped and this error is returned. The EdgeTtsTempArtifact Drop guard still guarantees the temp mp3 is cleaned up after the child exits.
Source
Thrown at crates/zeroclaw-channels/src/tts.rs:791
}
}
Ok(Err(err)) => {
reader.abort();
let _ = reader.await;
// Bound the kill-and-wait by the same absolute provider
// deadline used above, so a child that ignores the kill
// cannot hold `synthesize` past the provider timeout; the
// artifact guard's `Drop` still owns the final reap.
let _ = tokio::time::timeout_at(deadline, child.kill()).await;
let _ = tokio::time::timeout_at(deadline, child.wait()).await;
return Err(err).context("Failed to wait for edge-tts subprocess");
}
Err(_elapsed) => {
reader.abort();
let _ = reader.await;
let _ = tokio::time::timeout_at(deadline, child.kill()).await;
let _ = tokio::time::timeout_at(deadline, child.wait()).await;
bail!("Edge TTS subprocess timed out");
}
}
};
if !status.success() {
bail!("edge-tts failed (exit {}): {}", status, stderr);
}
let bytes = tokio::fs::read(&output_file)
.await
.context("Failed to read edge-tts output file")?;
Ok(bytes)
}
fn supported_voices(&self) -> Vec<String> {
// Edge TTS has many voices; return common defaults.
[View on GitHub (pinned to 88bb9c8533)
Solutions
- Reproduce manually: `edge-tts --text "hello" --voice en-US-AriaNeural --write-media /tmp/t.mp3` and time it.
- Upgrade the CLI (`pip install -U edge-tts`) when manual runs hang — upstream endpoint changes are the most frequent cause.
- Check network egress/DNS to the Microsoft speech service from the runtime host.
- Chunk long text into shorter per-call syntheses so each subprocess finishes well under 60s.
- Retry once; transient network stalls often clear.
Example fix
// before — one very long synthesis can outrun the fixed 60s subprocess timeout
let audio = mgr.synthesize(&long_article).await?;
// after — synthesize in chunks, each comfortably inside the budget
for chunk in split_unicode_chars(&long_article, 800) {
let part = mgr.synthesize(&chunk).await?;
send_audio(&part).await?;
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight smoke run: the CLI must answer quickly, catching broken
// installs and blocked egress before a real synthesis burns 60s.
async fn edge_tts_healthy() -> bool {
tokio::process::Command::new("edge-tts")
.arg("--list-voices")
.output()
.await
.map(|o| o.status.success())
.unwrap_or(false)
} Try / catch
const MAX_ATTEMPTS: u32 = 2;
for attempt in 1..=MAX_ATTEMPTS {
match mgr.synthesize(text).await {
Ok(audio) => return Ok(audio),
Err(err) if err.to_string().contains("Edge TTS subprocess timed out") && attempt < MAX_ATTEMPTS => {
tokio::time::sleep(Duration::from_secs(2)).await; // transient stall
}
Err(err) => return Err(err),
}
}
unreachable!() Prevention
- Keep per-call text short (chunk long input) so the subprocess finishes far under the fixed 60s budget.
- Pin and periodically upgrade edge-tts; endpoint changes make old versions hang.
- Verify egress to the Microsoft speech endpoint from the runtime host (containers with deny-by-default egress will black-hole it).
When it happens
Trigger: edge-tts hangs instead of exiting: a stalled connection to the Microsoft speech endpoint, DNS black-holing, a very long text whose synthesis exceeds 60s, an edge-tts version waiting on interactive input, or a pinned old version whose endpoint handshake no longer completes.
Common situations: Egress-filtered servers silently drop packets to speech.platform.bing.com so the CLI waits forever. Upstream Microsoft DRM/token changes periodically break old edge-tts releases, which then hang mid-request. Synthesizing whole articles in one call pushes past the fixed 60s budget.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- edge-tts failed (exit {}): {}
- Edge TTS binary_path must be a bare command name without pat
- Edge TTS binary_path must be one of {:?}, got: {raw_path}
- lucid command timed out after {}ms; failed to terminate and
- lucid command timed out after {}ms
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/128139913f089286.
Report an issue: GitHub.