vectordotdev/vector · error

Couldn't broadcast config changes.

Error message

Couldn't broadcast config changes.

What it means

`connect_diff` broadcasts a config-change diff over a broadcast channel and panics via `.expect("Couldn't broadcast config changes.")` if the send fails (src/topology/running.rs:936). With tokio broadcast channels, send only fails when there are no live receivers. Since topology reload expects its control-plane receivers to be alive, failure means the watching tasks have all exited.

Solutions

  1. Ensure the topology's control receivers (handle watchers) outlive any reload attempt before calling reload.
  2. Serialize reload against shutdown so connect_diff does not run during teardown.
  3. Recreate the running topology instead of reloading if its watchers are known dead.
Defensive patterns

Strategy: retry

Validate before calling

if watchers_alive(&topology.handles) { topology.connect_diff(diff).await; }

Try / catch

// tokio broadcast send failure = no receivers; detect via receiver count before reload
if topology.handles.receiver_count() == 0 { log::error!("watchers gone; cannot reload"); return; }

Prevention

When it happens

Trigger: Calling `reload_config_and_respawn` -> `connect_diff` after the diff-broadcast receiver tasks (e.g. handle-based watchers) have been dropped or shut down.

Common situations: A topology reload issued after the API/handle subscribers died; application shutting down while a reload races through connect_diff.

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


AI-assisted analysis of vectordotdev/vector@bdb87aeaa4 (2026-09-16). Data as JSON: /api/errors/1c7f8e9e9745093b. Report an issue: GitHub.

Appendix: source

Thrown at src/topology/running.rs:936

                                .map(|key| key.to_string()),
                        )
                        .collect(),
                    sink_keys: diff
                        .sinks
                        .changed_and_added()
                        .map(|key| key.to_string())
                        .chain(added_changed_tables.iter().map(|key| key.to_string()))
                        .collect(),
                    // Note, only sources and transforms are relevant. Sinks do
                    // not have outputs to tap.
                    removals,
                    type_names: self
                        .component_type_names
                        .iter()
                        .map(|(k, v)| (k.to_string(), v.clone()))
                        .collect(),
                })
                .expect("Couldn't broadcast config changes.");
        }
    }

    fn setup_outputs(&mut self, key: &ComponentKey, new_pieces: &mut builder::TopologyPieces) {
        let outputs = new_pieces.outputs.remove(key).unwrap();
        for (port, output) in outputs {
            debug!(component_id = %key, output_id = ?port, "Configuring output for component.");

            let id = OutputId {
                component: key.clone(),
                port,
            };

            self.outputs.insert(id, output);
        }
    }

    fn setup_inputs(

View on GitHub (pinned to bdb87aeaa4)