Integer overflow in code review: silent wraps, loud crashes, and five PRs to practice on
Integer overflow sounds like a C problem. Most of us write Python or TypeScript, where a plain integer never overflows, so it drops off the review radar. It shouldn't. Every language has places where a number gets squeezed into a fixed number of bits: a binary protocol, a typed array, a struct format string, a database column. We have five pull requests in the library where that squeeze is the bug, and they all look the same way in review: a small, confident line, a comment explaining why the width is enough, and tests that pass.
The two ranges you need to know
A signed 16-bit integer holds -32,768 to 32,767. A signed 32-bit integer holds -2,147,483,648 to 2,147,483,647, about 2.1 billion. Past those limits one of two things happens, and which one depends on the API, not on how careful the author was:
- It wraps. The value is reduced modulo 2^16 or 2^32 and comes back as a different, often negative, number. No error. JavaScript's
Int32ArrayandDataView.setInt16do this, and so do C, Java and Go. - It raises. Python's
struct.packrefuses the value withstruct.error: 'h' format requires -32768 <= number <= 32767. Nothing is corrupted, but the write never happens.
Neither is safe. The wrap gives you a wrong number that looks right. The raise gives you an outage on valid input. The review question is the same for both: does the type's range cover the range the requirements promise?
Shape one: the silent wrap in JavaScript
JavaScript numbers hold integers exactly up to 2^53 - 1, so an ordinary counter would be fine. Overflow only appears when someone opts into a fixed width, usually for memory or speed:
// Preallocated counter table: one slot per tracked video, zero-initialized.
const counts = new Int32Array(MAX_TRACKED_VIDEOS);
// ...
counts[slotFor(videoId)] += plays;The requirement says lifetime play counts must stay exact, and the busiest videos on the shard are already past two billion. The first increment over 2,147,483,647 lands near -2,147,483,648. No exception, no NaN. A dashboard shows a negative play count, or worse, a plausible positive one after a few more wraps. This is the same limit YouTube's view counter ran into in 2014, when "Gangnam Style" passed 2,147,483,647 views and YouTube had to move the counter to 64 bits.
The binary-protocol version is harder to see because the type is hidden in a method name:
// Flow fits an int16: pumps top out near ±30,000 mL/min.
view.setInt16(7, reading.flowMlMin);The new G-30 meters report up to ±48,000 mL/min. setInt16 writes 48,000 as -17,536, and the decoder reads it back as a strong reverse flow. The frame has the right length and every other field is correct, so nothing downstream can tell.
Practice on it: the play counter, the pump telemetry frame.
Shape two: the loud crash in Python
Python ints never overflow in memory, which makes reviewers relax. The overflow moves to the packing boundary:
# One record per tenant: the raw 16-byte tenant UUID followed by that
# tenant's lifetime API-call count.
_RECORD = struct.Struct(">16si")The i is a signed 32-bit integer. The spec says lifetime call counts are already past two billion. The first tenant to cross 2,147,483,647 makes serialize_checkpoint raise, no checkpoint is written for anyone on the shard, and a restart has to replay the entire event log. One valid number takes down the snapshot path for every tenant.
The same one-letter bug turns up in two more PRs. A queue-depth encoder packs depth as "<Ih" while the spec allows up to 1,000,000, so the crash arrives exactly when a real backlog builds and the metrics matter most. A drive-telemetry codec packs torque with ">h" for drives rated to ±60,000 cNm.
Practice on it: the metering checkpoint, the queue-depth encoder, the drive torque field.
The tell: a comment that sized the field for the old range
Read the comments in those snippets again. "Flow fits an int16: pumps top out near ±30,000." "Torque fits an int16, same as the temperature field: the servo line tops out near ±300 Nm." Each one was true when written. Each PR exists because the range changed: new meters, new drives, tenants that grew. The author, human or model, kept the old justification and the old width.
This is the pattern behind the most expensive integer overflow on record. In 1996 Ariane 5 reused Ariane 4's inertial reference software, which converted a 64-bit floating-point value into a 16-bit signed integer. On Ariane 4's trajectory the value always fit. Ariane 5 was faster, the conversion overflowed about 37 seconds after launch, and the rocket was destroyed. The code was correct for the range it was written for.
Time counters are the other classic. In 2015 the FAA ordered Boeing 787 operators to power-cycle the aircraft because a counter in the generator control units would overflow after 248 days of continuous power, which is 2^31 hundredths of a second.
What to check in the review
- Find every narrowing. Struct format codes (
h,i,H,I), typed arrays,DataViewsetters, column types, protobufint32fields. Plain Python and JavaScript arithmetic is not where the bug lives. - Put the type's range next to the requirement's range. Write both numbers down. If the requirement's maximum is bigger, it is a bug, not a style call.
- Distrust comments that justify a width. Ask which hardware, customer or year that range came from, and whether this PR changes it.
- Check the test values. A round-trip test with 1,000 or 30,000 proves nothing about 48,000. Ask for a test at the requirement's maximum and one past the type's maximum.
- Counters that only grow will get there. Lifetime totals, uptime in milliseconds, sequence numbers and IDs: estimate when they cross 2^31 and assume the service outlives that date.
The comment that scores, with our grader and with a colleague, names the value that breaks ("a G-30 reading of 48,000"), what happens to it ("written as -17,536, decoded as reverse flow"), and the fix ("widen to setInt32, frame grows to 11 bytes").
FAQ
What is integer overflow?
Integer overflow happens when a value is stored in a fixed-width integer type too small to hold it. A signed 16-bit integer holds -32,768 to 32,767 and a signed 32-bit integer holds -2,147,483,648 to 2,147,483,647. Depending on the language and API, a value past the limit either wraps around to a wrong number or raises an error.
Can integer overflow happen in Python or JavaScript?
Not in their ordinary integers: Python ints are arbitrary precision and JavaScript numbers hold integers exactly up to 2^53 - 1. It happens at fixed-width boundaries: Python's struct module, JavaScript typed arrays such as Int32Array, DataView setters, database columns and binary protocols. Python's struct raises struct.error; JavaScript typed arrays and DataView wrap silently.
How do you spot integer overflow in code review?
Find every place a number is narrowed into a fixed width (a struct format code, a typed array, a DataView setter, a column type) and compare the type's range with the range the requirements promise. Be most suspicious of a comment that justifies the width with the old range, and of tests that only use small values.
Try a review
Five PRs. Each one has a comment explaining why the width is fine.
Integer overflow in Python and TypeScript, silent and loud. Free, no signup. Canonical review after your verdict.
Browse the library →