embassy-rs/embassy · error
RpcService::run() must not be called concurrently
Error message
RpcService::run() must not be called concurrently
What it means
RpcService::run() is a single-consumer runner loop: the runner_state tracks a `running` flag and run() panics if invoked while a previous run() is still active. Only one run loop may drive the service at a time, since recovery and call dispatch state are singular.
Solutions
- Spawn run() exactly once and keep that task alive for the service lifetime.
- Await the existing run() future (or shut it down via RunGuard/Drop) before calling run() again.
- Use a supervisor task that owns run() so no other code path can start it.
Example fix
// before spawner.spawn(run_rpc(service.clone()).unwrap()); spawner.spawn(run_rpc(service.clone()).unwrap()); // panics // after spawner.spawn(run_rpc(service).unwrap()); // single runner
Defensive patterns
Strategy: validation
Validate before calling
// Wrap the runner so run() can only ever be spawned once:
use embassy_sync::once_lock::OnceLock;
static SPAWNED: OnceLock<()> = OnceLock::new();
fn spawn_runner_once(spawner, service) {
if SPAWNED.try_get().is_none() {
SPAWNED.init(());
spawner.must_spawn(service.run());
}
} Prevention
- Spawn run() in exactly one dedicated supervisor task
- Never call run() again before the previous future completes
- Use OnceLock/OnceCell style guards for one-shot spawns
- Review task spawn sites for duplicated runner tasks
When it happens
Trigger: Calling service.run(...) from two tasks/spawns concurrently, calling run() again before the first loop returns, or spawning run() in a loop without awaiting/joining the previous invocation.
Common situations: Accidentally spawning the RPC runner in two Embassy tasks; restarting the runner after a hot path without awaiting shutdown; firmware update code that re-inits the service while the old task still runs.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Bad Mode
- Bad mode
- PanicRawMutex locked from multiple contexts
- CallFuture polled after completion
- the AES is in use
AI-assisted analysis of embassy-rs/embassy@463a07b963 (2026-09-10).
Data as JSON: /api/errors/24e8eb28cedfd872.
Report an issue: GitHub.
Appendix: source
Thrown at embassy-sync/src/rpc_service.rs:519
/// This future is cancel-safe. A subsequent call to `run()` will recover the
/// previous state and resume processing any in-flight call.
pub async fn run(&self, state: &mut T) -> ! {
struct RunGuard<'a, M: RawMutex> {
runner_state: &'a Mutex<M, Cell<RunnerState>>,
}
impl<M: RawMutex> Drop for RunGuard<'_, M> {
fn drop(&mut self) {
self.runner_state.lock(|cell| {
let mut s = cell.get();
s.running = false;
cell.set(s);
});
}
}
let needs_recovery = self.with_runner_state(|s| {
if s.running {
panic!("RpcService::run() must not be called concurrently")
}
s.running = true;
s.needs_recovery
});
let _guard = RunGuard {
runner_state: &self.runner_state,
};
// If the previous runner was cancelled mid-job the caller might still
// be interacting with the slot. We must wait for it to finish (the caller
// always acks, either explicitly or via its Drop) and then clean up.
if needs_recovery {
self.slot.wait_ack_and_finish(&self.runner_state).await;
}
loop {
// Wait for a caller to submit a closure.
// This is a clean cancellation point because no job in flightView on GitHub (pinned to 463a07b963)