github/copilot-sdk · warning
runtime.shutdown timed out during Client::stop
Error message
runtime.shutdown timed out during Client::stop
What it means
During Client::stop, the code awaits runtime.shutdown() with a bounded timeout (RUNTIME_SHUTDOWN_TIMEOUT). If the async runtime does not finish shutting down in time, the library raises TimedOut and records it among shutdown errors rather than hanging forever.
Solutions
- Call Client::stop from outside the client's runtime (or a separate thread/runtime) to avoid self-deadlock
- Cancel/drop active streams, subscriptions, and pending requests before stop
- Increase or make configurable the shutdown timeout if teardown is legitimately slow
- Log the pushed shutdown errors to find which task refused to finish
Example fix
// before
client.stop().await?; // called from a task on client's runtime -> timeout
// after
let handle = tokio::spawn(async move { client.stop().await });
let _ = tokio::time::timeout(Duration::from_secs(10), handle).await; Defensive patterns
Strategy: try-catch
Validate before calling
// Rust: call stop from outside the client's runtime let not_inside_runtime = std::thread::current().name().is_none(); // or spawn stop on a dedicated thread
Try / catch
// Rust
if let Err(e) = client.stop().await {
tracing::warn!("stop incomplete: {e}; dropping client anyway");
} Prevention
- Never call Client::stop from within the client's own runtime tasks
- Close streams/subscriptions before stopping
- Treat stop timeouts as non-fatal teardown warnings and log pushed errors
When it happens
Trigger: Calling Client::stop while background tasks (writer actor, pending RPC handlers, leaked subscriptions) block runtime shutdown past the timeout, or awaiting shutdown from within a task on the same runtime.
Common situations: Calling stop from inside code running on the client's own runtime; open streams/handlers keeping tasks alive; a hung transport read preventing task completion; slow test teardown.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- failed to write a frame to the in-process runtime connection
- timed out gracefully shutting down runtime after
- writer actor has shut down
- writer actor dropped ack without responding
- Factory limit "timeoutSeconds" must not exceed
AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09).
Data as JSON: /api/errors/21a2da4aaa318c65.
Report an issue: GitHub.
Appendix: source
Thrown at rust/src/lib.rs:2873
match tokio::time::timeout(RUNTIME_SHUTDOWN_TIMEOUT, self.rpc().runtime().shutdown())
.await
{
Ok(Ok(())) => {
debug!(
elapsed_ms = runtime_shutdown_start.elapsed().as_millis(),
"Client::stop runtime shutdown complete"
);
}
Ok(Err(e)) => {
warn!(
elapsed_ms = runtime_shutdown_start.elapsed().as_millis(),
error = %e,
"runtime.shutdown failed during Client::stop",
);
errors.push(e);
}
Err(_) => {
let e = std::io::Error::new(
std::io::ErrorKind::TimedOut,
"runtime.shutdown timed out during Client::stop",
);
warn!(
elapsed_ms = runtime_shutdown_start.elapsed().as_millis(),
timeout = ?RUNTIME_SHUTDOWN_TIMEOUT,
error = %e,
"runtime.shutdown timed out during Client::stop",
);
errors.push(e.into());
}
}
}
let child = self.inner.child.lock().take();
let process_tree = self.inner.process_tree.lock().take();
*self.inner.state.lock() = ConnectionState::Disconnected;
*self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new());View on GitHub (pinned to cd8cf15dc3)