neondatabase/neon · critical

Non-overlapping bounds: other.max = {} was less than self.mi

Error message

Non-overlapping bounds: other.max = {} was less than self.min = {}

What it means

`ProtocolRange::highest_shared_version` computes the highest protocol version shared by two inclusive [min, max] ranges during the vm-monitor agent↔monitor handshake. This variant bails when `self.min > other.max`: the lowest version one side will speak is above the highest version the peer knows, so no common version exists to negotiate.

Source

Thrown at libs/vm_monitor/src/protocol.rs:219

impl fmt::Display for ProtocolRange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.min == self.max {
            f.write_fmt(format_args!("{}", self.max))
        } else {
            f.write_fmt(format_args!("{} to {}", self.min, self.max))
        }
    }
}

impl ProtocolRange {
    /// Find the highest shared version between two `ProtocolRange`'s
    pub fn highest_shared_version(&self, other: &Self) -> anyhow::Result<ProtocolVersion> {
        // We first have to make sure the ranges are overlapping. Once we know
        // this, we can merge the ranges by taking the max of the mins and the
        // mins of the maxes.
        if self.min > other.max {
            anyhow::bail!(
                "Non-overlapping bounds: other.max = {} was less than self.min = {}",
                other.max,
                self.min,
            )
        } else if self.max < other.min {
            anyhow::bail!(
                "Non-overlappinng bounds: self.max = {} was less than other.min = {}",
                self.max,
                other.min
            )
        } else {
            Ok(cmp::min(self.max, other.max))
        }
    }
}

/// We send this to the monitor after negotiating which protocol to use
#[derive(Serialize, Debug)]

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Upgrade the older component so both sides' [min, max] ranges overlap
  2. Or lower the upgraded side's minimum protocol version to still include the peer's maximum
  3. Log the serialized ProtocolRange from both ends of the handshake to identify which side is skewed
  4. If constructing ranges yourself, verify overlap before calling highest_shared_version

Example fix

// before: peer only supports up to v1.0, but our minimum is above that
let my_range = ProtocolRange { min: ProtocolVersion::V1_0.next(), max: /* ... */ };
my_range.highest_shared_version(&peer_range)?; // Non-overlapping bounds

// after: keep the minimum at the supported baseline until peers are upgraded
let my_range = ProtocolRange { min: PROTOCOL_MIN_VERSION, max: PROTOCOL_MAX_VERSION };
let version = my_range.highest_shared_version(&peer_range)?; // Ok(v1.0)
Defensive patterns

Strategy: validation

Validate before calling

fn ranges_overlap(a: &ProtocolRange, b: &ProtocolRange) -> bool {
    a.min <= b.max && a.max >= b.min
}

if !ranges_overlap(&my_range, &peer_range) {
    anyhow::bail!("no shared protocol version: {} vs {}", my_range, peer_range);
}
let v = my_range.highest_shared_version(&peer_range)?;

Type guard

fn ranges_overlap(a: &ProtocolRange, b: &ProtocolRange) -> bool {
    a.min <= b.max && a.max >= b.min
}

Prevention

When it happens

Trigger: Calling `highest_shared_version(&other)` where the receiver's `min` exceeds the peer's `max` — e.g. a monitor compiled with PROTOCOL_MIN_VERSION above v1.0 negotiating with an older agent whose PROTOCOL_MAX_VERSION is still v1.0.

Common situations: Version skew after upgrading the autoscaler agent but not the vm-monitor (or vice versa); a protocol bump shipped with a raised minimum before all peers updated; hand-built ProtocolRange values in tests that don't overlap.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/f3b722f314599228. Report an issue: GitHub.