astrid-runtime/astrid · error

IPC write timed out

Error message

IPC write timed out

What it means

write_message wraps the framed write (length prefix, payload, flush) in tokio::time::timeout(WRITE_TIMEOUT). If the write does not complete within that window, the library converts the elapsed future into a TimedOut io::Error with this message. It indicates the IPC peer or OS pipe/socket is not draining writes fast enough.

Solutions

  1. Investigate why the peer is not reading: check for a blocked or deadlocked read loop on the receiving side
  2. Increase WRITE_TIMEOUT if payloads are legitimately large or the host is slow
  3. Add backpressure/queueing in forward_outbound instead of blocking on a stalled peer
  4. Monitor and restart the unresponsive peer process

Example fix

// before
const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
// after: allow larger frames more time
const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
Defensive patterns

Strategy: retry

Validate before calling

// pre-check: is the peer still responsive?
let peer_alive = !child_try_wait()?.matches(&ExitStatus::default()); // or poll a ping/pong
if !peer_alive { restart_peer()?; }

Try / catch

match write_message(&mut writer, &payload).await {
    Err(e) if e.kind() == io::ErrorKind::TimedOut => {
        // peer stalled: attempt bounded retry, then recycle the peer
        if retries < MAX_RETRIES { retry_after_backoff().await?; }
        else { restart_peer().await?; }
    }
    other => other?,
}

Prevention

When it happens

Trigger: forward_outbound pushes messages to a peer whose read loop is stalled or blocked; the peer process is paused (SIGSTOP), deadlockeds, or its receive buffer is full; the socket/pipe is congested with a large backlog; WRITE_TIMEOUT is too small for very large payloads on slow media.

Common situations: Deadlocked child process not reading its IPC stdin/socket; very large messages exceeding the timeout budget on a loaded machine; peer stopped by a debugger breakpoint; system under heavy load starving the tokio runtime.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/9bf1a4b280f3fb8d. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-uplink/src/native/mod.rs:546

        frame
            .as_object_mut()
            .expect("wire frame is an object")
            .insert(
                "principal".to_owned(),
                serde_json::Value::String(principal.clone()),
            );
    }
    let bytes = serde_json::to_vec(&frame)
        .map_err(|error| std::io::Error::other(format!("serialize IPC message: {error}")))?;
    let len = u32::try_from(bytes.len())
        .map_err(|_| std::io::Error::other("IPC message exceeds 4 GiB"))?;
    tokio::time::timeout(WRITE_TIMEOUT, async {
        writer.write_all(&len.to_be_bytes()).await?;
        writer.write_all(&bytes).await?;
        writer.flush().await
    })
    .await
    .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "IPC write timed out"))?
}

fn publish_lifecycle(event_bus: &EventBus, topic: Topic, principal: &str, reason: Option<&str>) {
    let payload = match reason {
        Some(reason) => IpcPayload::Disconnect {
            reason: Some(reason.to_owned()),
        },
        None => IpcPayload::Connect,
    };
    let message = IpcMessage::new(topic, payload, uuid::Uuid::nil()).with_principal(principal);
    event_bus.publish(AstridEvent::Ipc {
        metadata: EventMetadata::new(EVENT_SOURCE),
        message,
    });
}

View on GitHub (pinned to affd8760f4)