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
- Upgrade the older component so both sides' [min, max] ranges overlap
- Or lower the upgraded side's minimum protocol version to still include the peer's maximum
- Log the serialized ProtocolRange from both ends of the handshake to identify which side is skewed
- 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
- Deploy agents and monitors together so PROTOCOL_MIN_VERSION/PROTOCOL_MAX_VERSION ranges always overlap
- Log both ProtocolRange values at handshake start so skew is diagnosable
- Never raise the minimum version until every peer is known to support it
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
- Non-overlappinng bounds: self.max = {} was less than other.m
- incompatible resource_multipler and spread_factor
- file cache size query returned no rows
- max file cache size query returned no rows
- invalid monitor Config: sys_buffer_bytes cannot be 0
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/f3b722f314599228.
Report an issue: GitHub.