# What invariants prevent a monotonic counter wrap from regressing sequence order? **Date of Record:** 2026 November 2 **Scope:** Applies to fixed-width counters used as monotonic sequence numbers, offsets, or tiebreakers (sequence ids, log offsets, fencing tokens) where readers assume monotonicity. Does not apply to 128-bit or arbitrary-precision counters. ## Core Distinction A fixed-width counter is only monotonic within its width. When it wraps, the next value is less than the last, and every ordering guarantee built on the counter is violated until the next wrap restores it — usually an interval of exactly one bit-width cycle. **Invariant.** The counter is monotonic for the deployment lifetime if and only if `rate * expected_lifetime * safety_factor < 2^bit_width`, or the reader performs wrap-aware comparison. **Violation.** A 32-bit microsecond timer wraps every 71 minutes. A cursor written before wrap at offset 100 returns nothing for the next 71 minutes because "greater than 100" excludes the post-wrap values 0..5. **Asymmetry.** A single comparison never reveals wrap. Only a comparison between two observations reveals it, and only if the reader checks direction. ## What Was Observed **Signature.** Sequence comparisons jumping backward. Sort-by-sequence returning recent items at the top of a list or dropped from a "greater than cursor" query. Fencing tokens appearing stale. **Trace.** 32-bit microsecond timer wraps at 2^32 µs = 71.6 min. A job scheduled before wrap writes offset 100; after wrap the same job writes offset 5. Cursor "greater than 100" returns nothing for 71 minutes. **Why monitoring stays green.** Both writes succeed. The counter is doing what it does. No error appears until a reader compares two values — often in a distant system, far from the writer. ## Condition of Validity The instance must ensure at least one of the following holds before it trusts a counter's ordering: 1. **Must size for lifetime.** `rate * expected_lifetime * safety_factor < 2^bit_width`, with safety_factor at least 10. 2. **Must compare wrap-aware.** Every reader compares modulo the bit width (RFC 1982 serial number arithmetic), not with plain less-than. 3. **Must widen before wrap.** The counter is widened to 64 or 128 bits before wrap becomes plausible on the deployment timescale. Condition 2 makes wrap harmless; 1 and 3 prevent it. The instance must know which applies before it interprets a sequence comparison as evidence of recency. ## Decision Boundary - Width with margin > 10x lifetime: safe by construction. - Width with margin < 1x lifetime: wrap is guaranteed before decommission. - Wrap-aware reader regardless of width: safe, but every reader must be wrap-aware, not only some. - Widened and non-wrap-aware: safe, provided widening preceded the first production wrap. ## Operational Effect The defect couples a storage decision (bit width) to a reader assumption (monotonicity) that is rarely written down. Remediation is widening or wrap-aware comparison. The trap is to widen without auditing readers. Widening does not break old data, but it silently converts the wrap point into a new one and leaves wrap-aware code doing unnecessary arithmetic. ```python from dataclasses import dataclass class CounterWrapError(RuntimeError): """Raised when a counter's width cannot absorb expected lifetime traffic.""" @dataclass(frozen=True) class CounterGate: bit_width: int observed_rate: int expected_lifetime_s: int safety_factor: float = 10.0 def verify(self) -> bool: capacity = 2 ** self.bit_width expected = self.observed_rate * self.expected_lifetime_s * self.safety_factor if expected >= capacity: raise CounterWrapError( f"expected {int(expected)} >= capacity {capacity}: wrap within lifetime for width {self.bit_width}" ) return True def dispatch_counter_mode(gate: CounterGate) -> str: try: gate.verify() except CounterWrapError: return "wrap-aware-reader" return "plain" ```