stalwartlabs/stalwart · critical

Node id {node_id} exceeds {MAX_NODE_ID}, panicking to avoid

Error message

Node id {node_id} exceeds {MAX_NODE_ID}, panicking to avoid data corruption

What it means

At config parse time the server builds a snowflake ID generator and installs a node id taken from the validated config. Snowflake ids embed the node id, and exceeding MAX_NODE_ID would make ids collide or overflow across cluster nodes, corrupting data. The code deliberately panics rather than starting with an unsafe node id, warning that this is to avoid data corruption.

Source

Thrown at crates/common/src/config/inner.rs:53

    cache::{Cache, CacheWithTtl},
    snowflake::{MAX_NODE_ID, SnowflakeIdGenerator},
    tls::build_tls_connector,
};

impl Data {
    pub async fn parse(bp: &mut Bootstrap) -> Self {
        // Parse certificates
        let mut certificates = AHashMap::new();
        let mut subject_names = AHashSet::new();
        parse_certificates(bp, &mut certificates, &mut subject_names).await;
        if subject_names.is_empty() {
            subject_names.insert("localhost".into());
        }

        // Build and test snowflake id generator
        let node_id = bp.node_id();
        if node_id > MAX_NODE_ID {
            panic!("Node id {node_id} exceeds {MAX_NODE_ID}, panicking to avoid data corruption");
        }
        SnowflakeIdGenerator::set_node_id(node_id as u64);
        let id_generator = SnowflakeIdGenerator::new();
        if !id_generator.is_valid() {
            panic!("Invalid system time, panicking to avoid data corruption");
        }

        // Initialize apps
        let applications = WebApplications::new();
        applications.reload(bp).await;

        let blocked_ips = BlockedIps::parse(bp).await;
        let lookup_stores = LookupStores::build(bp).await;

        Data {
            spam_classifier: ArcSwap::from_pointee(SpamClassifier::default()),
            tls_certificates: ArcSwap::from_pointee(certificates),
            tls_self_signed_cert: build_self_signed_cert(

View on GitHub (pinned to e962003857)

Solutions

  1. Lower the configured node id to a value within range (0..=MAX_NODE_ID, per the message) and restart.
  2. If automating deployment, map instance numbers into the valid range (e.g. modulo 1024) before assigning node ids.
  3. Ensure each node still gets a unique in-range id to preserve snowflake uniqueness.

Example fix

# before (config.toml)
node-id = 4096
# after
node-id = 7
Defensive patterns

Strategy: validation

Validate before calling

const MAX_NODE_ID: u32 = 1023; // match the snowflake layout
let node_id = config.node_id();
if node_id > MAX_NODE_ID {
    eprintln!("node-id {node_id} exceeds {MAX_NODE_ID}");
    std::process::exit(2);
}

Type guard

fn valid_node_id(v: u32) -> Option<u32> { (v <= 1023).then_some(v) }

Try / catch

// it is a startup panic, not catchable; validate config before launch:
// wrap server start in a supervisor that surfaces the panic and exits non-zero

Prevention

When it happens

Trigger: Setting a `node-id` (or cluster node id) value in the server configuration that is greater than MAX_NODE_ID (the bitmask capacity of the snowflake layout, e.g. >1023 for a 10-bit node field). `Config::parse` panics during startup.

Common situations: Operators hard-code large node ids when scripting multi-node deployments (e.g. using instance numbers past the limit), or misconfigure a numeric field intended to be small, or reuse a generator config from another product with a wider node field.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06). Data as JSON: /api/errors/139da2fbf57a8b84. Report an issue: GitHub.