TIL: Number.MAX_SAFE_INTEGER is 2**53 - 1

I was filling out a form today and a friend joked, “You should put Number.MAX_SAFE_INTEGER in the budget field.” Woah! I didn’t know we had that constant.

Number.MAX_SAFE_INTEGER was added to JavaScript in 2015 as part of ES6. The constant is new to me, but the value is a number I know by heart: 2**53 - 1.

When I worked at Twitter, we switched from 32-bit IDs generated by MySQL to 64-bit IDs generated by Snowflake. Late in the project–just a few weeks before launch–we hit a small road bump: we could not use the new IDs in the browser.

With the Snowflake IDs, sometimes you would like a tweet and the request would fail, “tweet not found”. You would scroll the timeline and duplicate tweets would appear. These bugs and others were caused by the way JavaScript represents numbers.

JavaScript implents IEEE-754, which uses 64 bits to represent all its numbers–big, small, floating point, integer. Practically this means that you do not get 64 bits to represent integers. Of the 64 bits, 1 bit represents the sign, 11 bits are used to represent the exponent of a number, and the remaining 52 bits are what’s left for the mantissa (what you typically think of as the actual number). This means you can only represent integers faithfully up to 2**53 - 1, Number.MAX_SAFE_INTEGER, 9007199254740991. (This was 2010 and support for bigints didn’t land until 2018-2020.)

The memory format of an IEEE 754 double-precision floating-point value. Illustration from Wikipedia

Above that limit, the numbers behave in unexpected ways:

> Number.MAX_SAFE_INTEGER
9007199254740991
> Number.MAX_SAFE_INTEGER + 1
9007199254740992
> Number.MAX_SAFE_INTEGER + 2
9007199254740992
> Number.MAX_SAFE_INTEGER + 3
9007199254740994

Tweets in the API came back like this:

{
  "id": 915710611442491393,
  "text": "..."
}

but the browser would parse the id as 915710611442491400. That’s close! But close isn’t usually what you want for IDs.

At the time, we solved the problem by adding "id_str" with a string representation of the ID. We couldn’t change or remove "id" because it was a widely used public API, but that vestigial field was a perennial cactus underfoot. People would do things like use the field, and get unexpected results when the ID was not the ID.

See also the archived source for snowflake and the archived twitpocalypse.com.