A Field Guide to Floating Point: Eight Real Bugs and What They Teach Us
Floating point arithmetic is one of those topics that every programmer has heard warnings about. The abstract list of dangers is familiar: addition isn't associative, large values swallow small ones, big integers can't be represented, and NaN can propagate chaos. But without concrete examples, these warnings can feel academic. What do these issues actually look like when they break a real program?
Here are eight real-world cases where floating point caused tangible problems, drawn from reports by working engineers.
Floating point isn't malicious—it's precise about its limits
Before diving in, it's worth clearing the air. Floating point is not a broken or random tool. It's a remarkably efficient and standardized system for numerical computation, governed by IEEE 754. The arithmetic is deterministic: 0.1 + 0.2 will always yield the same result across architectures. It might not be the result you expected, but it's predictable.
The inherent issue is that computers can't store arbitrary precision for every number efficiently; approximation and rounding are unavoidable in numerical work. The goal here isn't to condemn floating point, but to understand exactly where and why it fails, so you know when to reach for a different tool.
The odometer that froze permanently
A developer reported an odometer that continuously added small increments to a 32-bit float to measure distance. Simulating this by adding 1 cm at a time, the odometer gets stuck after only about 262 kilometers of travel. The error isn't a subtle drift—the value simply stops increasing entirely.
The root cause is the spacing between representable floats. Near the value 262144.0, consecutive 32-bit floats are 0.03125 apart. When you try to add 0.01, it doesn't round up to the next representable number. The sum just stays at 262144.0. It's no coincidence that 262144 is a power of two (2^18); the gap between floats increases after each power of two, and at 2^18 the jump becomes too large for small increments to register.
A simple fix is switching to a 64-bit double, which yields a result with only ~17 cm of error. An even more robust approach is to increment in larger chunks, for example every 50 cm, which eliminates the problem entirely. A third alternative is to use integers—track distances as multiples of the smallest unit you care about.
Large integers fall apart in JavaScript
JavaScript only has floating point numbers; it has no native integer type. The largest integer that can be safely represented as a 64-bit float is 2^53. Tweet IDs, however, are larger than that. The Twitter API returns them as both integers and strings precisely because of this limitation—the integer version will silently corrupt in JavaScript.
The issue isn't confined to JavaScript itself. Since JSON (JavaScript Object Notation) is often decoded in a JavaScript-compatible way, tools like jq can mangle large integer values in transit, turning 1612850010110005250 into 1612850010110005200—a loss of 50. Notably, Python's json module handles the same number correctly because it supports big integers. This discrepancy has caused real problems for developers transmitting large IDs or pointer addresses as JSON.
Catastrophic cancellation in statistical calculations
A naive single-pass algorithm for calculating variance uses a running sum and a running sum of squares. For small datasets, it works fine. But when computing the variance of 100,000 large numbers clustered around 100000000, the algorithm produces a variance of negative value, which is mathematically impossible.
This is a classic case of catastrophic cancellation. The sum_of_squares value grows to about 10^21 (2^69). At that magnitude, the gap between consecutive floats is enormous—around 2^46. When the algorithm subtracts two such large, imprecise numbers, the error in the result dwarfs the actual signal. Welford's algorithm was designed to avoid exactly this issue, but for most users, the practical solution is to let a scientific computing library like Numpy handle variance.
Identical calculations can differ across platforms
A puzzling class of bugs arises when frontend and backend code perform the "same" floating point operation but get different results. IEEE 754 guarantees determinism for basic arithmetic, but variations creep in at other levels. Mathematical functions in the C standard library (like sin or log) are not covered by the standard and can behave differently across implementations like glibc and musl. Furthermore, some x86 instructions internally use 80-bit precision for certain operations, which can yield slightly different results than a strict 64-bit double calculation. This can lead to confusing user-visible discrepancies.
The "Deep Space Kraken" strikes in simulation
Kerbal Space Program, a space simulation game, famously had a bug called the "Deep Space Kraken." When a player's ship moved extreme distances from the origin, floating point errors would compound, leading to the ship being torn apart by phantom forces. This is a common failure pattern in games, astrophysics, and large-scale simulations: numbers that are far from the origin become so large that the precision needed for relative distances is lost. The same fundamental issue created the famous "Far Lands" terrain distortion in Minecraft at extreme map coordinates.
Timestamps break past their precision limit
The current Unix epoch in nanoseconds is around 1.67 × 10^18, which is well beyond the 2^53 integer limit for a 64-bit float. The next representable number after it is roughly 256 nanoseconds later. Trying to store such a value as a float would introduce significant timing errors. This is why production time libraries use integers, not floats, for timestamps. It's also a poignant reminder that there are cases where integer representation is simply the better, safer choice.
Layout logic and the mystery of the leftover space
Even everyday calculations with small numbers can fail. Consider trying to split a page of width 13.716 into columns of width 4.572. Using floor(page_width / column_width) and the modulo operator % might suggest there is no leftover space, because the result of the modulo operation rounds to zero. However, a direct calculation of 13.716 - 3 * 4.572 yields a tiny negative number, revealing that the columns are actually too wide. The lesson here is to never derive the same value using two different floating point operations, as the results won't match. This kind of bug can cause significant headaches in page layout and CAD applications.
The infinite collision loop
A common and naive approach to collision detection is to decrement a variable by a small amount and check for exact equality with a target. A Python simulation might start with a = 1000` and subtract a small value in a loop until `a` is equal to 0. The loop never terminates. Due to floating point rounding, the value skips past zero, jumping from a tiny positive number to a larger negative one.
The correct approach is to check if a value is within a small epsilon of the target, or better yet, to structure the logic around a comparison like while a > 0, which is robust to the inherent imprecision of the decrement operation.
These examples don't even touch on the weirder corners of floating point—NaNs, infinities, signed zero, and subnormals—all of which have their own potential for chaos. But they illustrate a clear pattern: floating point is a powerful tool when its precision limits are respected and disastrous when its assumptions are ignored. The key is knowing when to use it, and when to rely on integers or other representations instead.



