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
- Lower the configured node id to a value within range (0..=MAX_NODE_ID, per the message) and restart.
- If automating deployment, map instance numbers into the valid range (e.g. modulo 1024) before assigning node ids.
- 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
- Cap generated node ids at the snowflake node-field width in deployment tooling.
- Document the valid node-id range next to the config sample.
- Assign unique in-range ids per node to preserve id uniqueness.
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
- Invalid system time, panicking to avoid data corruption
- Failed to read {yaml_path:?}
- Invalid jump position
- unwrap_tls called on non-TLS acceptor
- Sieve test failed: {}
AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06).
Data as JSON: /api/errors/139da2fbf57a8b84.
Report an issue: GitHub.