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

  1. Retry the request once after a short backoff; transient app-loop stalls often clear.
  2. Raise the timeout value passed to dispatch_to_app_with_timeout if the workload is legitimately slow.
  3. Profile or inspect what the app loop was doing at the time (tracing logs) to find the stall.
  4. 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

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

Related errors


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/88ad6f055ecb7e40. Report an issue: GitHub.