The report is a clue. Find out what was counted and where.
This is an illustrative upload incident. A worker adds each received chunk to a signed
32-bit byte counter. The count is positive until it approaches 2,147,483,647,
then a later update makes the reported total negative. A 32-bit signed integer has a
finite range; the service may be doing exactly the arithmetic its type defines, while
violating the application's expectation that a byte count stays nonnegative.
The scenario does not establish the cause of any real outage. A negative dashboard value could also come from a bad unit conversion, an incorrect delta, a decoder bug, or a metric label collision. Start by locating the raw counter, its declared type, every update and conversion, and the first timestamp at which stored state differs from the expected cumulative bytes.
- Observed clue
- Reported cumulative bytes become negative near 2 GiB.
- Illustrative starting count
2,147,000,000 bytes- Next chunk
1 MiB = 1,048,576 bytes- Question
- What result should the type hold, and what should the service do at its limit?
One bit chooses the sign, leaving the rest to encode magnitude around zero.
A signed n-bit two's-complement integer has 2ⁿ bit patterns. The top
bit has weight −2ⁿ⁻¹; the remaining bits have positive weights 2⁰ through 2ⁿ⁻². Its range is therefore −2ⁿ⁻¹ through 2ⁿ⁻¹ − 1. There is one more negative value than positive values because zero
takes one of the nonnegative patterns.
For 8 bits, 0111 1111₂ = 127, while 1000 0000₂ = −128. Add one
to 127 in an 8-bit signed representation and the bits roll to 1000 0000₂,
interpreted as −128. In two's-complement arithmetic, negating a value means invert every
bit and add one; that is why the negative endpoint has no positive counterpart in the same
width.
For the 32-bit upload counter, minimum is −2,147,483,648 and maximum is 2,147,483,647. The illustrated update is not a close-call rounding issue: its
exact mathematical sum is beyond the type's largest representable value.
Go's bounded integers and TypeScript's Number fail differently.
TypeScript's number is JavaScript's IEEE 754 binary64 floating-point type. It
is not a fixed-width signed integer. Every integer is exactly represented only from −(2⁵³ − 1) through 2⁵³ − 1, or ±9,007,199,254,740,991. Beyond that safe range, a value
can still be finite, but adjacent integers may map to the same Number. For example, Number.MAX_SAFE_INTEGER + 1 and + 2 both evaluate to 9,007,199,254,740,992.
That is precision loss, not 32-bit wrap. A TypeScript counter holding
2.148 billion bytes is represented exactly by Number; the familiar int32 boundary does not
change Number arithmetic. Use Number.isSafeInteger for integer values that
must stay exact, or use bigint for larger exact integer arithmetic. BigInt and
Number arithmetic cannot be mixed implicitly. If large integers arrive as JSON numeric tokens,
precision can already be lost during parsing; a string representation may be needed at that
boundary.
Go's int32 and int64 are fixed-width signed integers. The
language specification defines signed overflow as deterministic; it does not panic. The
32-bit result wraps as shown above. Go's architecture-sized int may be 32 or 64
bits depending on the target, so protocol and storage boundaries should use explicit widths
when they matter. A conversion to a narrower integer type can also truncate, so validate before
converting.
At 2⁵³, neighboring integer values stop being distinguishable. Number does not wrap at int32 boundaries.
At 2,147,483,647 + 1, signed arithmetic yields −2,147,483,648. No runtime panic signals the application error.
A value can fit the machine type and still exceed a product, protocol, allocation, or storage limit.
Predict the result before you let the calculator show it.
Choose a small signed width and add a delta. The lab calculates the exact mathematical sum first, then shows its two's-complement signed interpretation at the selected width. This models fixed-width bit arithmetic only; a production program should normally detect an invalid update rather than rely on wrap.
The wrap result is a representation demonstration. For a counter, reject or handle the update when the intended value is out of range.
Check the update at the point where the value can cross its limit.
The TypeScript helper rejects non-safe integers before addition and checks the result after addition. The Go helper checks the int64 bounds before evaluating the sum; its separate int32 example reproduces the upload counter's wrapped result. Explicit types help describe bounds, while checked arithmetic enforces the application's policy.
The snippets show distinct numeric models: safe integer validation for Number, checked int64 addition, and defined fixed-width overflow.
export type SignedWidth = 8 | 16 | 32;
export function isSafeByteCount(value: number): boolean {
return Number.isSafeInteger(value) && value >= 0;
}
export function checkedAdd(current: number, delta: number): number | undefined {
if (!Number.isSafeInteger(current) || !Number.isSafeInteger(delta)) return undefined;
const result = current + delta;
return Number.isSafeInteger(result) ? result : undefined;
}
export function addSigned(width: SignedWidth, current: bigint, delta: bigint): bigint {
const modulus = 1n << BigInt(width);
const signBit = 1n << BigInt(width - 1);
const unsigned = (((current + delta) % modulus) + modulus) % modulus;
return unsigned >= signBit ? unsigned - modulus : unsigned;
}
export const example = {
width: 32 as SignedWidth,
max: (1n << 31n) - 1n,
chunk: 1_048_576n,
before: 2_147_000_000n
};
package main
import (
"errors"
"math"
)
func checkedAddInt64(current, delta int64) (int64, error) {
if delta > 0 && current > math.MaxInt64-delta {
return 0, errors.New("byte count exceeds int64 maximum")
}
if delta < 0 && current < math.MinInt64-delta {
return 0, errors.New("byte count falls below int64 minimum")
}
return current + delta, nil
}
func demonstrateInt32Wrap() (int32, int32) {
before := int32(2_147_000_000)
chunk := int32(1_048_576)
return before, before + chunk // defined signed overflow: -2_146_918_720
}
Follow the value, not just the final graph.
For the current language semantics, see the TypeScript Handbook's everyday types (which explains that TypeScript shares JavaScript runtime behavior), MDN's Number.MAX_SAFE_INTEGER, and the Go specification sections on integer overflow and conversions. Go's math integer-limit constants provide auditable bounds such as MaxInt32 and MaxInt64. These
references describe language behavior; your application still has to decide which values
are valid.