tracel-ai/burn · error
Service call failed
Error message
Service call failed
What it means
`RemoteClient::ensure_connected` submits a blocking connect/handshake task to the remote service's runner thread via `handle.submit_blocking(...)` and unwraps the returned Option with `expect("Service call failed")`. The service call channel returns `None` when the service/runner is shut down or the submission fails (channel closed), so the unwrap panics. It signals that the background service backing this client is no longer alive.
Solutions
- Recreate the client/service connection instead of reusing the stale handle.
- Check the remote server is running and reachable before creating the client.
- Keep the service/runner alive for the lifetime of all handles (don't drop the owner early).
- Inspect server logs for the underlying connection or backend failure.
Example fix
// before let client = RemoteClient::create(&config); drop(runner); // service shut down client.ensure_connected(); // panics: Service call failed // after let client = RemoteClient::create(&config); client.ensure_connected(); // while service is alive // if shutdown happened: create a fresh client instead of reusing
Defensive patterns
Strategy: try-catch
Validate before calling
// check liveness before calling
if client.service_alive() /* or try a cheap submit that tolerates None */ {
client.ensure_connected();
} Try / catch
// wrap the panic boundary so the app can reconnect
let connected = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| client.ensure_connected()));
if connected.is_err() {
let client = RemoteClient::create(&config); // rebuild service and retry
client.ensure_connected();
} Prevention
- Keep the service/runner alive for the lifetime of all handles
- Never use client handles after shutdown/drop of the owning runtime
- Monitor remote server health before issuing device enumeration
- Catch panics at the connection boundary to enable reconnect logic
When it happens
Trigger: Calling `ensure_connected()` (directly or via `RemoteDevice::defaults`/`enumerate`) after the service runner thread has stopped (client dropped/shutdown, panic in the service, or the handle's channel was closed), so `submit_blocking` yields `None`.
Common situations: Using a remote client/handle after the runtime was torn down; server process crashed mid-session; holding a cloned handle past shutdown; environment where the compute server failed to start (bad address, missing backend).
Related errors
- Service call failed
- Can't register manually a tensor on a remote channel.
- Invalid response type for ReadTensor
- adaptive_avg_pool2d_backward: unsupported dtype
- adaptive_avg_pool2d: unsupported dtype
AI-assisted analysis of tracel-ai/burn@d16f7ba2ed (2026-09-05).
Data as JSON: /api/errors/f31ba67b5bf75e46.
Report an issue: GitHub.
Appendix: source
Thrown at crates/burn-remote/src/client/base.rs:32
impl RemoteClient {
pub fn init(device: RemoteDevice) -> Self {
// `DeviceHandle::new` initializes the service the first time it's called for a given
// device id. `RemoteService::init` is deliberately cheap — it records the endpoint but
// does NOT connect, because cubecl holds a process-global lock across it; the actual
// connect + handshake happens lazily on first use (or via `ensure_connected`).
// Subsequent calls return a handle to the existing service.
let handle = DeviceHandle::<RemoteService>::new(device.to_id());
Self { device, handle }
}
/// Force the lazily-established connection to be opened now, populating the device's
/// settings/device-count cells. Used by the settings path (`RemoteDevice::defaults` /
/// `enumerate`), which needs the handshake reply before any op has flushed. Runs the
/// connect on the service's runner thread, so it can't sit under cubecl's global lock.
pub(crate) fn ensure_connected(&self) {
self.handle
.submit_blocking(|s| s.ensure_connected())
.expect("Service call failed");
}
/// Establish the session asynchronously, the way the browser requires.
///
/// The connect + handshake cannot block the single browser thread, so it runs off the device
/// handle: the service hands back the connection parameters, the network round-trip happens
/// with `.await`, and the opened session is installed back into the service. A no-op once the
/// session is up.
#[cfg(target_family = "wasm")]
pub(crate) async fn connect_async(&self) {
use crate::client::service::wasm_connect;
let Some(plan) = self
.handle
.submit_blocking(|s| s.wasm_connect_plan())
.expect("Service call failed")
else {
return;View on GitHub (pinned to d16f7ba2ed)