FuelLabs/fuel-core · warning

The block height subscription channel was closed: {:?}

Error message

The block height subscription channel was closed: {:?}

What it means

Subscriber::wait_for_block_height registers a oneshot channel in a BTreeMap keyed by Reverse(block_height) and awaits it. The future resolves with Err (channel closed) when every stored Sender is dropped without sending — the sender lives in the subscription service's handler map, so closure means the service dropped the handlers (e.g., shutdown, cleanup, or map replacement) before the height was reached.

Source

Thrown at crates/fuel-core/src/graphql_api/block_height_subscription.rs:76

    ) -> anyhow::Result<()> {
        let future = {
            let mut inner_map = self.inner.write();

            if inner_map.current_block_height >= block_height {
                return Ok(());
            }

            let (tx, rx) = oneshot::channel();
            let handlers = inner_map
                .tx_handles
                .entry(Reverse(block_height))
                .or_default();
            handlers.push(tx);
            rx
        };

        future.await.map_err(|e| {
            anyhow::anyhow!("The block height subscription channel was closed: {:?}", e)
        })
    }

    pub fn current_block_height(&self) -> BlockHeight {
        self.inner.read().current_block_height
    }
}

#[derive(Debug, Default)]
struct HandlersMapInner {
    tx_handles: BTreeMap<Reverse<BlockHeight>, Vec<oneshot::Sender<()>>>,
    current_block_height: BlockHeight,
}

impl HandlersMapInner {
    fn new(current_block_height: BlockHeight) -> Self {
        Self {
            tx_handles: BTreeMap::new(),

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Retry the wait after confirming the node is still running and producing/importing blocks.
  2. Before waiting, short-circuit if current_block_height() already satisfies the target.
  3. Treat the error as transient and re-subscribe rather than as data corruption.

Example fix

// before
subscriber.wait_for_block_height(h).await?;

// after
if subscriber.current_block_height() >= h {
    return Ok(());
}
subscriber.wait_for_block_height(h).await?;
Defensive patterns

Strategy: retry

Validate before calling

// Fast-path when the height is already reached; the channel only closes on service teardown.
if subscriber.current_block_height() >= wanted_height {
    return Ok(());
}
subscriber.wait_for_block_height(wanted_height).await?;

Try / catch

loop {
    if subscriber.current_block_height() >= wanted { break; }
    match subscriber.wait_for_block_height(wanted).await {
        Ok(()) => break,
        Err(e) if e.to_string().contains("channel was closed") => {
            // transient teardown of the subscription service: back off and retry
            tokio::time::sleep(Duration::from_millis(500)).await;
            continue;
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: Awaiting a block height while the BlockHeightSubscriptionService is stopped/dropped or clears its handler map — node shutdown, service restart, or internal cleanup removing pending handlers.

Common situations: GraphQL subscriptions or internal awaiters pending on future heights during node shutdown or service re-initialization; long waits for heights that are never produced (paused block production) followed by cleanup.

Related errors


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/3315956f9486297f. Report an issue: GitHub.