Integer Overflow, BigInt and Safe Limits by Language

Numbers lecture 2: INT_MAX + 1 in C, Java, C#, Swift, Dart, PHP and Python, why JSON.parse changes 19-digit IDs, strings vs BigInt, and SQLite limits.

Series: Numbers & Money lecture 2/6 (beginner). For new developers who store prices, counters or IDs in integers and assume that integers, unlike floats, are always exact. After this lecture you know how big each language's integers are, what happens when a calculation goes past the limit, and how to move 19-digit IDs through JSON and JavaScript without losing digits.

Lecture 1 showed that floats are approximate. Integers are exact, but only inside their range, and every language does something different at the edge: C gives undefined behaviour, Java wraps around silently, Swift stops the program, PHP turns the number into a float, and JavaScript quietly rounds it. If you build an order system, a payment API, a counter that runs for years, or a front end that receives database IDs as JSON, you will meet at least one of these. Everything below was run on an Apple Silicon Mac (M1 Pro), macOS 26, with Apple clang 21, OpenJDK 26, .NET 10 (SDK 10.0.403), Swift 6.4.2, Dart 3.11.5, PHP 8.5.6, Python 3.9.6 and 3.12.14, JavaScriptCore jsc and sqlite 3.51.0. Paths are shortened to ~/demo/.

Your first overflow in 5 minutes

A price of 2,000,000 (cents, won, yen, it does not matter) times a quantity of 1,500 is 3,000,000,000. Save this as first_ov.c:

#include <stdio.h>
int main(void) {
    int price = 2000000;
    int qty = 1500;
    int total = price * qty;
    printf("total = %d\n", total);
    long long ok = (long long)price * qty;
    printf("ok    = %lld\n", ok);
    return 0;
}
$ clang -std=c17 -Wall -Wextra -o first_ov first_ov.c && ./first_ov
total = -1294967296
ok    = 3000000000

No warning at compile time, no crash, just a negative total. An int in C on this Mac is 32 bits, so it holds -2,147,483,648 to 2,147,483,647. Three billion does not fit; the bits that remain read as -1,294,967,296. This is called integer overflow: the result of a calculation is outside the range of its type.

The fix is to do the multiplication in a wider type. (long long)price * qty converts one operand to 64 bits before multiplying. Writing long long total = price * qty; is too late: the multiplication already happened in 32 bits.

Add -fsanitize=undefined (UBSan, the undefined-behaviour sanitizer) and clang tells you the exact line:

$ clang -std=c17 -Wall -Wextra -fsanitize=undefined -o first_ov_ub first_ov.c && ./first_ov_ub
first_ov.c:5:23: runtime error: signed integer overflow: 2000000 * 1500 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior first_ov.c:5:23
total = -1294967296
ok    = 3000000000

How big is an integer? Sizes per language

Language Default integer Range Bigger option
C (clang, macOS arm64) int 32-bit ±2.1×10^9 long / long long 64-bit (sizeof 8 and 8 here)
Java int 32-bit ±2.1×10^9 long 64-bit, BigInteger
C# int 32-bit ±2.1×10^9 long 64-bit, decimal, BigInteger
Swift Int 64-bit on 64-bit platforms ±9.2×10^18 Int64, no built-in bigint
Dart (native) int 64-bit ±9.2×10^18 BigInt
Dart (compiled to JavaScript) int is a JS number exact to 2^53 BigInt
PHP int 64-bit (PHP_INT_SIZE 8) ±9.2×10^18 bcmath, gmp
Python int unlimited not needed
JavaScript Number (a double) exact to 2^53 - 1 BigInt
SQLite INTEGER up to 8 bytes ±9.2×10^18 store as TEXT

Two numbers are worth memorising: 2,147,483,647 (2^31 - 1, the 32-bit limit) and 9,007,199,254,740,991 (2^53 - 1, the largest integer a double holds without gaps). The 64-bit limit is 9,223,372,036,854,775,807.

What happens at the edge: seven languages compared

We computed MAX + 1 and 2,000,000 × 1,500 everywhere. Outputs are copied from the runs.

C: signed overflow is undefined behaviour (UB): the standard does not say what happens, and the optimizer may assume it never does. On this Mac INT_MAX + 1 printed -2147483648, but that is luck, not a rule (see C undefined behaviour and optimization for a loop that changes with -O2). Unsigned overflow is defined: UINT_MAX + 1 printed 0. To check without UB, use __builtin_add_overflow(a, 1, &r) (a clang/gcc builtin), which returned true for INT_MAX. With -fno-sanitize-recover=all UBSan stops the program at the first report; exit status was 134.

Java: wraps around, always, silently.

a + 1 = -2147483648
price * qty = -1294967296
long wrong = -1294967296          // long wrong = price * qty;
long right = 3000000000           // long right = (long) price * qty;
Math.abs(Integer.MIN_VALUE) = -2147483648
java.lang.ArithmeticException: integer overflow     // Math.addExact(a, 1)
java.lang.ArithmeticException: integer overflow     // Math.multiplyExact(price, qty)

Math.abs of the smallest int is still negative, because +2,147,483,648 does not exist in an int. The *Exact methods (Java 8 and later) are the safe versions.

C#: wraps by default; checked turns overflow into an exception, and constants are checked at compile time.

price * qty = -1294967296
OverflowException: Arithmetic operation resulted in an overflow.     // checked(price * qty)
Program.cs(1,9): error CS0220: The operation overflows at compile time in checked mode

The last line came from int x = int.MaxValue + 1;, which does not compile. A project-wide switch, <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow> in the .csproj, makes all arithmetic checked; a fresh dotnet new console project does not set it.

Swift: overflow traps, meaning the program stops on purpose. The constant case does not even compile:

ovc.swift:1:26: error: arithmetic operation '2147483647 + 1' (on type 'Int32') results in an overflow

With values known only at run time, the program printed about to multiply and then exited with status 133 (128 + signal 5, SIGTRAP), in both -Onone and -O builds. When you want wrapping, you say so: a &+ 1 gave -2147483648, and a.addingReportingOverflow(1) gave (-2147483648, true).

Dart: depends on where it runs. On the Dart VM, int is 64-bit and wraps: 9223372036854775807 + 1 printed -9223372036854775808. Compiled to JavaScript with dart compile js, an int is a JS number, so the compiler rejects literals it cannot represent:

Error: The integer literal 9007199254740993 can't be represented exactly in JavaScript.

When the value came from int.parse instead, the compiled code (run in jsc, not a browser) printed 9223372036854776000 for the 64-bit maximum and 9007199254740992 for 2^53 + 1, and identical(1, 1.0) became true. Same source, different numbers: test Flutter web separately.

PHP: an int that overflows becomes a float.

var_dump(PHP_INT_MAX + 1);            float(9.223372036854776E+18)
var_dump(PHP_INT_MAX + 1 == PHP_INT_MAX);   bool(true)
(int)(PHP_INT_MAX + 1)  -> Warning: The float 9.223372036854776E+18 is not representable as an int, cast occurred
                           int(-9223372036854775808)
intdiv(PHP_INT_MIN, -1) -> Fatal error: Uncaught ArithmeticError: Division of PHP_INT_MIN by -1 is not an integer

No error, but the float has only 53 bits of precision, so PHP_INT_MAX + 1 == PHP_INT_MAX is true. Casting back gave a warning on 8.5.6; we did not test whether older versions print it. bcadd("9223372036854775807", "1") and gmp_add both printed the exact 9223372036854775808.

Python: integers never overflow. (2**63 - 1) + 1 printed 9223372036854775808, and 2**64 * 2**64 printed all 39 digits. Limits only appear where Python hands numbers to a fixed-size world: struct.pack('<i', 2**31) raised struct.error: 'i' format requires -2147483648 <= number <= 2147483647, and (2**64).to_bytes(8, 'little') raised OverflowError: int too big to convert.

Decision diagram for MAX + 1. If the integer is not fixed-size, it grows or changes type: Python int grows, PHP int becomes a float, a JavaScript Number rounds above 2 to the 53. If it is fixed-size and the operator is checked, the program stops: Swift's default operators trap with exit status 133, Java's Math.addExact and C#'s checked throw. If not checked and it is a signed int in C, it is undefined behaviour that UBSan reports. Otherwise it wraps around silently: Java, C#, Dart on the VM, Swift's &+ and unsigned C.
What MAX + 1 did in each runtime we ran. Four different behaviours; only one of them stops you.

JavaScript: MAX_SAFE_INTEGER and IDs that lose digits

A JavaScript Number is a double (lecture 1). Its 53-bit significand holds every integer up to 2^53 exactly; above that, the gaps between doubles are 2, then 4, then 8…

$ jsc safe.js
9007199254740991 true              // Number.MAX_SAFE_INTEGER, === 2**53 - 1
9007199254740992 9007199254740992 true   // 2**53, 2**53 + 1, and they are equal
true false                         // Number.isSafeInteger(2**53 - 1), (2**53)
9007199254740992 9007199254740992 9007199254740994 9007199254740996 9007199254740996   // 2**53 + 0..4

This matters most for IDs. Many databases and ID schemes (64-bit auto-increment keys, Snowflake-style IDs from distributed systems) produce numbers above 2^53. The server sends correct JSON, and the browser's JSON.parse silently changes them:

const body = '{"id": 1234567890123456789, "order_id": 9007199254740993, "sid": "1234567890123456789"}';
const o = JSON.parse(body);
print(o.id, o.order_id, o.sid);
print(JSON.stringify(o));
1234567890123456800 9007199254740992 1234567890123456789
{"id":1234567890123456800,"order_id":9007199254740992,"sid":"1234567890123456789"}

The ID changed by 11, the order ID by 1, and no error appeared. If the client sends that ID back, it updates the wrong row, or none. The string sid survived. Python's json.loads and PHP 8.5's json_decode read 9007199254740993 correctly, because their integers are bigger; PHP turned 12345678901234567890 (above 64 bits) into float(1.2345678901234567E+19) unless you pass JSON_BIGINT_AS_STRING, which gave string(20) "12345678901234567890". See PHP prepared statements changing JSON number types for a related trap on the server side.

The fix, in order of preference:

  1. Send IDs as strings. Your API returns "id": "1234567890123456789". IDs are labels, not quantities; you never add them. This works in every client.
  2. Use BigInt for arithmetic. BigInt("1234567890123456789") + 1n printed 1234567890123456790. BigInt has its own rules, all checked in jsc:
1n + 1                 TypeError: Invalid mix of BigInt and other type in addition.
JSON.stringify({x:10n})  TypeError: JSON.stringify cannot serialize BigInt.
Math.sqrt(16n)         TypeError: Conversion from 'BigInt' to 'number' is not allowed.
  1. Read the raw text while parsing. Newer engines pass the source text of each number to the JSON.parse reviver, and JSON.rawJSON writes a BigInt back as a bare number. Both worked in this Mac's jsc:
const r = JSON.parse(body, (k, v, ctx) => k === 'id' ? BigInt(ctx.source) : v);
print(String(r.id), typeof r.id);                                // 1234567890123456789 bigint
print(JSON.stringify({ id: JSON.rawJSON(r.id.toString()) }));    // {"id":1234567890123456789}

These two are recent additions to the language; check that every browser and Node.js version you support has them before relying on them (we ran only jsc). Option 1 has no such caveat.

Another quiet 32-bit limit: bitwise operators convert to 32-bit integers. (2147483647 + 1) | 0 printed -2147483648, and Math.imul(2000000, 1500) printed -1294967296. The JavaScript performance and numbers lecture goes further into Number and BigInt.

Databases: SQLite's INTEGER at the limit

SQLite's INTEGER is a signed 64-bit value. At the edge it does three different things:

sqlite> SELECT 9223372036854775807 + 1, typeof(9223372036854775807 + 1);
9.22337203685478e+18|real
sqlite> SELECT sum(x) FROM (SELECT 9223372036854775807 AS x UNION ALL SELECT 1);
Runtime error near line 4: integer overflow
sqlite> SELECT total(x) FROM (SELECT 9223372036854775807 AS x UNION ALL SELECT 1);
9.22337203685478e+18

The + operator quietly switches to a float, sum() raises an error, and total() always returns a float. Inserting 12345678901234567890 into an INTEGER column stored it as real; the same digits in quotes were also converted to real, because the column has integer affinity. Store over-64-bit values in a TEXT column.

Primary keys at the limit:

sqlite> CREATE TABLE t(id INTEGER PRIMARY KEY, n INTEGER);
sqlite> INSERT INTO t(id, n) VALUES (9223372036854775807, 1);
sqlite> INSERT INTO t(n) VALUES (2);
sqlite> SELECT * FROM t;
712334064962770860|2
9223372036854775807|1

When the largest rowid is taken, SQLite picks an unused one at random. With AUTOINCREMENT, which promises never to reuse or go backwards, the same insert failed: Error: stepping, database or disk is full (13). You will not reach 9.2×10^18 by counting, but you can if someone inserts a huge ID by hand. For SQL basics, see SELECT, WHERE and ORDER BY.

Common mistakes and which type to use

Mistake What actually happens Fix
long long t = a * b; with int a, b (C, Java long t = a * b) multiplication overflows in 32 bits first: -1294967296 widen one operand: (long long)a * b
Counting milliseconds in an int 2^31 ms is about 24.8 days use 64-bit
Math.abs(x) to make a value non-negative Math.abs(Integer.MIN_VALUE) is -2147483648 check the minimum, or use long
Numeric IDs in JSON for a JS client 1234567890123456789 arrives as 1234567890123456800 send IDs as strings
Mixing BigInt and Number TypeError: Invalid mix of BigInt and other type convert explicitly; keep one type per value
Trusting PHP int arithmetic past 2^63 silently becomes float, == PHP_INT_MAX is true bcmath or gmp
One Dart codebase for VM and web 9223372036854775807 becomes 9223372036854776000 on web keep values under 2^53, or BigInt

Choosing a type:

You are storing Use Avoid
Counts, quantities, minor currency units under about 9×10^15 64-bit integer (long, Int, int in PHP/Dart VM, INTEGER) 32-bit int when totals can grow
IDs from a database or another service string in JSON and in JS; 64-bit int on the server JS Number
Integers you must do maths on beyond 2^63 BigInteger, Python int, BigInt, gmp floats
Values that must never overflow silently Java *Exact, C# checked, Swift default operators, __builtin_*_overflow plain + in C/Java/C#

Money specifically, with cents and decimal types, is lecture 4. For the type basics, see Java types and PHP types.

Measured: how many IDs survive JSON.parse, and what BigInt costs

How many IDs come through JSON.parse intact? We generated 100,000 random integers in each of five ranges with Python (random.seed(7)), wrote them as JSON numbers and as strings, parsed both in jsc, and compared each parsed number with its string using BigInt.

Bar chart. Up to 2 to the 53 minus 1, 100 percent of IDs survive. From 2 to the 53 to 2 to the 54, 49.99 percent. From 2 to the 54 to 2 to the 56, 16.72 percent. Snowflake-sized IDs between 2 to the 59 and 2 to the 61, 0.53 percent. 19-digit IDs between 10 to the 18 and 2 to the 63, 0.19 percent.
Share of 100,000 random integers per range that came out of JSON.parse unchanged in jsc. Seed 7; each parsed value compared with its string form using BigInt.
Range Survived Largest error
up to 2^53 - 1 100,000 (100%) 0
2^53 to 2^54 49,994 (49.99%) 1
2^54 to 2^56 16,718 (16.72%) 4
2^59 to 2^61 (Snowflake-sized) 526 (0.53%) 128
10^18 to 2^63 (19 digits) 189 (0.19%) 512

Above 2^53 the survivors are only the IDs that happen to be multiples of the gap size. For 19-digit IDs, 99.8% come out wrong by up to 512.

What does BigInt cost? Summing 0 to 9,999,999 in jsc, median of 7 runs on an Apple Silicon Mac (M1 Pro): Number 3.2 ms, BigInt 301.3 ms, both giving 49999995000000. The JIT optimizes the Number loop very well, so treat the roughly 94× gap as an upper bound, not a constant. Python's arbitrary-size int, adding 1,000,000 values to a running total (median of 7):

Starting value Python 3.9.6 Python 3.12.14
0 27.7 ms 26.6 ms
2**64 28.1 ms 26.9 ms
2**256 30.6 ms 29.0 ms
2**4096 141.8 ms 138.0 ms

Big integers are not free, but up to a few hundred bits Python barely notices. Use BigInt where you need it, not everywhere.

Honest limits / version notes

  • C integer sizes are for clang on macOS arm64. long is 32-bit on 64-bit Windows (not run here).
  • Signed overflow results in C are what this compiler did at -O0; the standard promises nothing.
  • Swift printed no message for the trap in our captured output, only exit status 133; Xcode shows more in its debugger (not run here).
  • JSON.parse source-text access and JSON.rawJSON were checked only in this Mac's jsc. Python's 4,300-digit limit: 3.12.14 raised ValueError: Exceeds the limit (4300 digits) for integer string conversion, 3.9.6 printed 5,001 digits, because the limit arrived in 3.9.14.
  • Dart-to-JavaScript output was run in jsc, not in a browser. Node.js was not run.
  • Timings are from one machine; compare ratios.

Exercises

1. In Java, which of these print 3000000000?

int price = 2_000_000, qty = 1500;
long a = price * qty;
long b = (long) price * qty;
long c = (long) (price * qty);

2. Your API returns {"id": 9007199254740993} and the web client sends the ID back to delete a record. What does jsc turn the ID into, and what should the API change?

3. In Python, x = 2**31 is fine. Name one place where the same value raises an error, and quote the error.

Answers

1. Only b. Our run printed -1294967296 3000000000 -1294967296 for a b c. a and c both multiply in 32 bits first; the cast in c comes after the overflow.

2. JSON.parse gave 9007199254740992, one less, so the client would delete record ...992, not ...993. Return the ID as a string, "id": "9007199254740993", and keep it a string in the client.

3. struct.pack('<i', 2**31) raised struct.error: 'i' format requires -2147483648 <= number <= 2147483647. The same happens when you write to a binary file format, a C library or a 32-bit database column.

Next

Lecture 3 is about rounding: why Python's round(2.5) is 2, what toFixed really does, and how tax per line and tax per total give different results.

FAQ

Why does JSON.parse change the last digits of my ID?

JavaScript numbers are doubles, which hold every integer exactly only up to 2^53 - 1 (Number.MAX_SAFE_INTEGER, 9007199254740991). Beyond that the parser picks the nearest double. In jsc, JSON.parse turned 1234567890123456789 into 1234567890123456800 and 9007199254740993 into 9007199254740992. Send large IDs as JSON strings and convert with BigInt only if you need arithmetic.

What happens when an int overflows in Java or C#?

By default both wrap around silently: Integer.MAX_VALUE + 1 is -2147483648, and 2,000,000 * 1,500 gives -1294967296. Java's Math.addExact and Math.multiplyExact throw ArithmeticException: integer overflow; C# throws OverflowException inside checked(...) or with CheckForOverflowUnderflow enabled.

Do Python integers overflow?

No. Python int grows as needed: (2**63 - 1) + 1 printed 9223372036854775808. Limits appear at the edges: struct.pack('<i', 2**31) raises struct.error, and since 2022 security releases (3.11, 3.10.7, 3.9.14 and later) converting an int with more than 4,300 digits to a string raises ValueError by default.

  • #numbers
  • #beginner
  • #integer overflow
  • #BigInt
  • #JSON
  • #JavaScript

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑