nautechsystems/nautilus_trader · error · DydxError::Nautilus
Broadcast semaphore closed: {e}
Error message
Broadcast semaphore closed: {e} What it means
Before broadcasting, `broadcast_with_retry` acquires a tokio semaphore permit to serialize transaction broadcasts (ensuring sequence N is broadcast before N+1 is allocated). If `semaphore.acquire()` returns `AcquireError` — which happens only when the semaphore has been closed — the error is wrapped as 'Broadcast semaphore closed'. A closed semaphore means the broadcaster is shutting down or was closed via `close()`, so no further broadcasts can proceed.
Source
Thrown at crates/adapters/dydx/src/execution/broadcaster.rs:180
///
/// # Returns
///
/// The transaction hash on success.
///
/// # Errors
///
/// Returns error if all retries are exhausted or a non-retryable error occurs.
pub async fn broadcast_with_retry(
&self,
tx_manager: &TransactionManager,
msgs: Vec<Any>,
operation_name: &str,
) -> Result<String, DydxError> {
// Acquire semaphore to serialize broadcasts.
// This ensures sequence N is fully broadcast before sequence N+1 is allocated.
let _permit =
self.broadcast_semaphore.acquire().await.map_err(|e| {
DydxError::Nautilus(anyhow::anyhow!("Broadcast semaphore closed: {e}"))
})?;
log::debug!("Acquired broadcast permit for {operation_name}");
// Flag to track if we need to resync sequence before the next attempt.
// Set by should_retry when a sequence mismatch is detected.
let needs_resync = Arc::new(AtomicBool::new(false));
let needs_resync_for_retry = Arc::clone(&needs_resync);
// Clone values that need to be moved into closures
let grpc_client = self.grpc_client.clone();
let rate_limiter = Arc::clone(&self.rate_limiter);
let op_name = operation_name.to_string();
let operation = || {
// Clone captures for the async block
let needs_resync = Arc::clone(&needs_resync);
let grpc_client = grpc_client.clone();View on GitHub (pinned to 18893faf8b)
Solutions
- Ensure no broadcasts are submitted after the adapter's stop/disconnect lifecycle has begun
- Check application code for calling `close()` on the semaphore/broadcaster while workers are still active
- Gracefully drain pending broadcasts before initiating shutdown
- Treat this error as terminal: do not retry the broadcast; re-establish the execution client if needed
Example fix
// before
// shutdown path
tokio::spawn(async move { broadcaster.broadcast_with_retry("order", op).await; }); // races close
// after
broadcaster.broadcast_with_retry("order", op).await?; // complete work first
broadcaster.shutdown().await; // then close semaphore Defensive patterns
Strategy: try-catch
Validate before calling
// Guard against broadcasting during shutdown
if shutting_down.load(std::sync::atomic::Ordering::Acquire) { anyhow::bail!("shutting down, skipping broadcast"); } Type guard
fn broadcaster_open(b: &Broadcaster) -> bool { !b.is_closed() } Try / catch
match broadcaster.broadcast_with_retry("order", op).await {
Err(e) if e.to_string().contains("semaphore closed") => log::warn!("broadcast skipped: shutting down"),
other => other?,
} Prevention
- Drain in-flight broadcasts before closing the semaphore
- Never submit broadcasts after stop()/disconnect() begins
- Use lifecycle events to gate order submission during shutdown
When it happens
Trigger: Calling `broadcast_with_retry` after another task called `broadcast_semaphore.close()` — typically during adapter shutdown/stop, or a raced shutdown where a broadcast is attempted concurrently with teardown.
Common situations: Sending a final order/cancel during engine shutdown, a task holding a reference to the broadcaster after `disconnect()`/`stop()`, or reconnect logic racing with a close.
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
- partitioned cancel failed: {}
- Transaction broadcast failed: code={}, log={}
- error.to_string()
- slots lock poisoned
- Close command should not be drained
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/3fc9dacdedca87a9.
Report an issue: GitHub.