skip to content

In PHP, what happens when an integer calculation exceeds PHP_INT_MAX, and how do you work with integers that large?

level: middleimportance: should knowfreq 40%

answer

  1. no exception, no wraparound
  2. result silently becomes float
  3. PHP_INT_MAX: 2^63 - 1 on 64-bit
  4. floats exact only up to 2^53
  5. gmp or bcmath for bigger

basics

~20 s

PHP does not throw or wrap on integer overflow: the result silently becomes a float. On 64-bit builds PHP_INT_MAX is 9223372036854775807, and floats above 2^53 cannot represent every integer, so precision is lost; use GMP or bcmath strings for larger values.

solid answer

~40 s

PHP's `int` is a signed 64-bit integer on 64-bit builds (`PHP_INT_MAX` is `9223372036854775807`, `PHP_INT_SIZE` is `8`). When `+`, `-` or `*` produces a result outside that range, PHP neither throws nor wraps: it returns a **float**, and an integer literal too large for `int` is parsed as a float too. The danger is silent precision loss: a double holds every integer only up to 2^53, so `PHP_INT_MAX + 1 === PHP_INT_MAX + 2` is `true`. `is_int()` checks start failing, and converting such a float back to `int` wraps around and, since PHP 8.5, emits a warning. `intdiv(PHP_INT_MIN, -1)` is the case that throws, an `ArithmeticError`. For large values use the **GMP** extension (`gmp_init()`, `gmp_add()`, overloaded operators on `GMP` objects) or **bcmath** with numeric strings, and keep big identifiers as strings end to end.

code

php · 14 lines
php
<?php
$a = PHP_INT_MAX + 1;
$b = PHP_INT_MAX + 2;

var_dump($a);            // float(9.223372036854776E+18)
var_dump($a === $b);     // bool(true): both are 2^63 as floats

try {
    intdiv(PHP_INT_MIN, -1);
} catch (ArithmeticError $e) {
    echo $e->getMessage(), PHP_EOL; // Division of PHP_INT_MIN by -1 is not an integer
}

echo gmp_strval(gmp_init(PHP_INT_MAX) + 2), PHP_EOL; // 9223372036854775809

go deeper

for a junior

Recall that PHP_INT_MAX is about 9.2e18 on 64-bit and that going past it turns the result into a float instead of throwing.

for a middle

Explain why that float loses precision above 2^53, why intdiv(PHP_INT_MIN, -1) throws, and what PHP 8.5 changed about converting such floats back to int.

for a senior

Keep large identifiers as strings, guard native arithmetic before it overflows, and choose GMP or bcmath where exact large values are part of the domain.

for a principal

Define where native integers are safe and where arbitrary precision is mandatory, and make that visible in types and data contracts across services.

## The integer range A PHP `int` is a signed integer of the platform's word size. On the 64-bit builds used almost everywhere today: - `PHP_INT_SIZE` is `8` (bytes); - `PHP_INT_MAX` is `9223372036854775807` (2^63 - 1); - `PHP_INT_MIN` is `-9223372036854775808`. On a 32-bit build `PHP_INT_MAX` is `2147483647`, which is why older code and some Windows builds hit overflow much sooner. ## What overflow does Many languages either wrap around (`MAX + 1` becomes `MIN`) or throw. PHP does neither. When an integer operation's exact result does not fit, PHP computes it as a **float** instead: ```php <?php var_dump(PHP_INT_MAX); // int(9223372036854775807) var_dump(PHP_INT_MAX + 1); // float(9.223372036854776E+18) var_dump(9223372036854775808); // float(9.223372036854776E+18): literal too big for int var_dump(is_int(PHP_INT_MAX * 2)); // bool(false) ``` No warning, no notice. The value's type simply changes. ## Why the float is a problem A double has a 53-bit significand, so it can represent **every** integer only up to 2^53 (`9007199254740992`). Above that, consecutive integers collapse onto the same float. Near `PHP_INT_MAX` the gap between adjacent floats is 2048: | Expression | Result | |---|---| | `PHP_INT_MAX + 1` | `float`, exactly 2^63 | | `PHP_INT_MAX + 2` | `float`, also 2^63 | | `PHP_INT_MAX + 1 === PHP_INT_MAX + 2` | `true` | | `(int) (PHP_INT_MAX + 1)` | wraps to `PHP_INT_MIN`; PHP 8.5 adds a warning | The last row matters in PHP 8.5: converting a float that cannot be represented as an `int`, whether by an explicit `(int)` cast or implicitly, now emits `Warning: The float ... is not representable as an int, cast occurred`. Earlier versions produced the wrapped value silently. The one arithmetic operation that throws instead of producing a float is `intdiv(PHP_INT_MIN, -1)`: the true result, 2^63, is not an integer PHP can return, and `intdiv()` always returns `int`, so it throws `ArithmeticError`. ## Where this shows up in real code 1. **Large identifiers.** 64-bit IDs generated by distributed systems, or unsigned 64-bit database columns, can exceed `PHP_INT_MAX` or pass through a float somewhere in decoding. Keep them as **strings** end to end and compare them as strings. 2. **Multiplying counters.** Byte counts, microsecond timestamps multiplied by factors, or factorials overflow quietly and keep "working" with wrong low digits. 3. **Checks that assume `int`.** A value that became a float fails `is_int()` and strict `int` parameter declarations, often far from the arithmetic that caused it. ## Arbitrary-precision options PHP ships two extensions for numbers beyond native range; both must be enabled in the build: | Extension | Values | Interface | Best for | |---|---|---|---| | **GMP** | integers of any size | `GMP` objects with overloaded operators, plus `gmp_add()`, `gmp_mul()`, `gmp_strval()` | big integers, cryptography-style arithmetic, bit operations | | **bcmath** | decimals of any size and scale | numeric strings (`bcadd()`, `bcmul()`) and, since PHP 8.4, `BcMath\Number` objects | exact decimals such as money, fixed-scale results | ```php <?php $id = gmp_init('9223372036854775807'); $next = $id + 1; // GMP object, exact echo gmp_strval($next), PHP_EOL; // 9223372036854775808 ``` ## Guarding the native path When staying with native `int` is required, check before you compute rather than after: compare against `intdiv(PHP_INT_MAX, $factor)` before multiplying, or assert `is_int()` on results that must stay integral. Detecting overflow afterwards is unreliable because the float result may already have lost the digits you would need to notice it. ## Common mistakes - **Expecting an exception.** No `OverflowException` or `ArithmeticError` is raised for `+`, `-` or `*`; the type silently becomes `float`. - **Expecting wraparound.** `PHP_INT_MAX + 1` is not `PHP_INT_MIN`; only an explicit conversion of the resulting float back to `int` wraps. - **Trusting the float's digits.** Printing the overflowed value with `var_dump()` shows a plausible number whose low digits are already gone. - **Assuming 64 bits everywhere.** Code that must run on a 32-bit build should use `PHP_INT_MAX` and `PHP_INT_SIZE` instead of hard-coded limits.

  • In PHP, why does PHP_INT_MAX + 1 === PHP_INT_MAX + 2 evaluate to true?
    Both sums overflow, so PHP computes them as floats. Near 2^63 adjacent doubles are 2048 apart, so the exact results 2^63 and 2^63 + 1 both round to the same float, 2^63. The comparison is between two identical floats, so `===` is true.
  • In PHP, when would you choose GMP over bcmath for large numbers?
    GMP for integers: it is backed by the GMP library, supports overloaded operators on `GMP` objects and integer-specific operations such as modular exponentiation and bit functions. bcmath for exact decimals with a fixed scale, such as money, through numeric-string functions or `BcMath\Number`. Both require the extension to be enabled in the PHP build.

saying these in an interview costs you the question

  • Says PHP throws an OverflowException when an int exceeds PHP_INT_MAX.
  • Believes PHP_INT_MAX + 1 wraps around to PHP_INT_MIN.
  • Thinks a float can hold any 64-bit integer exactly.
  • Stores 64-bit identifiers as floats after decoding.
  • Assumes PHP_INT_MAX is the same on every build.