herdrdev/herdr · error · io::Error
timed out waiting for app response after {} ms
Error message
timed out waiting for app response after {} ms What it means
The API server forwards each request to the app over an mpsc channel and waits on response_rx with a timeout. If the app event loop does not produce a response within the timeout, recv_timeout returns Timeout and dispatch_to_app maps it to ErrorKind::TimedOut with 'timed out waiting for app response after {ms} ms'. It means the app accepted (or at least did not reject) the dispatch but never replied in time — typically a busy or stalled app loop, not a protocol violation.
Source
Thrown at src/api/server.rs:852
if let Err(err) = api_tx.send(ApiRequestMessage {
request,
respond_to,
response_write_complete,
stream_active,
}) {
if let Some(active) = request_active {
active.store(false, Ordering::Release);
}
return error_response_json(
request_id,
"server_unavailable",
format!("failed to dispatch request: {err}"),
);
}
let response = match timeout {
Some(timeout) => response_rx.recv_timeout(timeout).map_err(|err| match err {
std::sync::mpsc::RecvTimeoutError::Timeout => std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"timed out waiting for app response after {} ms",
timeout.as_millis()
),
),
std::sync::mpsc::RecvTimeoutError::Disconnected => std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"app response channel closed",
),
}),
None => response_rx
.recv()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::BrokenPipe, err)),
};
match response {
Ok(response) => response,View on GitHub (pinned to f457cff4f2)
Solutions
- Retry the request once after a short backoff; transient app-loop stalls often clear.
- Raise the timeout value passed to dispatch_to_app_with_timeout if the workload is legitimately slow.
- Profile or inspect what the app loop was doing at the time (tracing logs) to find the stall.
- If it reproduces consistently for one request type, that request likely deadlocks or does very expensive work in the app — fix that path.
Example fix
// before let resp = dispatch_to_app_with_timeout(&app_tx, req, Duration::from_millis(100))?; // after let resp = dispatch_to_app_with_timeout(&app_tx, req, Duration::from_millis(5000))?;
Defensive patterns
Strategy: retry
Validate before calling
null
Try / catch
match dispatch_to_app_with_timeout(&tx, req, timeout) {
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
// retry once with backoff; if it repeats, dump app-loop tracing and report a stall
}
other => other,
} Prevention
- Size timeouts to worst-case app-loop latency, not happy-path latency.
- Keep the app event loop free of long synchronous work.
- Log dispatch latency so slow drift is visible before timeouts start.
When it happens
Trigger: handle_request / dispatch_stream_open / dispatch_stream_frame called while the app's event loop is blocked (long render, debugger pause, GC-style stall, or a deadlock); or the timeout passed to dispatch_to_app_with_timeout is shorter than the app's actual processing time for that request.
Common situations: Very large workspace/pane counts making the app loop slow; the app paused in a debugger or under SIGSTOP; a request that triggers heavy synchronous work (e.g. scrolling a huge scrollback); CI machines under load making the deadline miss; too-aggressive timeouts after a config change.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out reading api request
- api request line is too large
- app response channel closed
- timed out reading stream frame header
- timed out reading stream frame body
AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28).
Data as JSON: /api/errors/88ad6f055ecb7e40.
Report an issue: GitHub.