When JSON.parse turns one ID into another: 64-bit IDs measured

By Filewhisk Team Published

A quiet snowflake service loses 5% of its IDs to JSON.parse, a busy one 80%, and every changed ID becomes a copy of another. Auto-increment IDs are safe.

Everyone who has met this bug has the same sentence for it: JavaScript numbers cannot hold 64-bit integers, so an ID loses its last digits. It is true, and it is also the whole of what most write-ups say. It does not tell you which of your IDs are actually at risk, how often, or what the damage looks like — and the answers are more uneven than “always” suggests. So we measured it.

What the standard promises

RFC 8259, the JSON standard, is explicit that this is a known limit. Section 6 says that “good interoperability can be achieved by implementations that expect no more precision or range than” IEEE 754 double-precision numbers provide, and that integers in the range −(253)+1 to (253)−1 are interoperable — implementations will agree exactly on their values. That is 9,007,199,254,740,991. Above it, the standard promises nothing, and a browser's JSON.parse stores every number as a double.

How we measured

We generated seven sets of IDs, wrote each as a JSON array of plain numbers, and parsed it with JSON.parse in Chrome 153. For every ID we compared the number that came out with the number that went in, as exact integers, and counted how many changed and how many distinct IDs the array held before and after. We counted the same thing in Python, whose float is a separate implementation of the same IEEE 754 double, and the two counts agreed in all seven runs.

The snowflake sets follow the layout Twitter made popular and Discord and many others copied: 41 bits of milliseconds since an epoch (here 4 November 2010), 10 bits for the data centre and worker, and a 12-bit sequence number that counts up when the same worker issues more than one ID in the same millisecond. Timestamps fall on 26 September 2026, and each ID was issued at a random millisecond, the way independent requests arrive. The other sets are uniformly random 63-bit numbers and plain counters.

What came out

IDsCountChanged by JSON.parseDistinct values lost
Snowflake, 1 worker, 100 per second10,000497 (5.0%)497
Snowflake, 1 worker, 1,000 per second10,0003,711 (37.1%)3,711
Snowflake, 1 worker, 5,000 per second50,00040,079 (80.2%)40,079
Snowflake, 10 workers, 1,000 per second each100,00036,662 (36.7%)36,662
Random 63-bit numbers100,00099,413 (99.4%)0
Counter starting at 1015100,0000 (0%)0
Counter starting at 253100,00050,000 (50%)49,999

Three things are worth reading out of that table, because none of them is what the usual warning suggests.

Quiet snowflake services mostly survive, busy ones do not

At 100 IDs a second on one worker, 95% of the IDs came through untouched. It looks like the bug does not exist — which is exactly why it goes unnoticed in testing. The reason is arithmetic. These IDs are about 2.1×1018, which sits between 260 and 261, where doubles are spaced 256 apart. An ID survives only if it is a multiple of 256. The low 22 bits of a snowflake hold the worker and the sequence; a worker number by itself is a multiple of 4,096, and a sequence of zero adds nothing, so the first ID a worker issues in any millisecond is exact. Every further ID in the same millisecond has a small nonzero sequence and is rounded.

So the share of IDs that break is the share that are not the first of their millisecond, and that depends on load: 5% at 100 a second, 37% at 1,000, 80% at 5,000. A staging environment with a handful of requests will never show it. Production at a busy hour will.

Every changed ID becomes a copy of another one

The last column is the one that matters most. In every snowflake run, the number of distinct values lost equals the number of IDs changed. The 10,000 IDs at 1,000 a second came out as only 6,289 different numbers. Two IDs a few units apart round to the same multiple of 256, so the ID is not merely wrong; it is identical to a different record's ID. Used as a key in a map, a cache or a set, the two records become one, and nothing throws an error.

Random IDs are wrong but not duplicated

Uniformly random 63-bit IDs almost all change (99.4%; the rest happen to have enough trailing zero bits), but not one pair merged in 100,000. Random values are far apart compared with the rounding step, so an altered ID is usually a number nobody issued. That is an inference from the result, not something we tested: it suggests a lookup would fail rather than quietly return another record. Either way the ID is no longer the one you were sent.

Counters are fine, for a very long time

A counter starting at 1015 lost nothing in 100,000 IDs, because every value is below 253. The trouble starts at that boundary: from 253 onwards half the counter values were changed and 49,999 of 100,000 collapsed into a neighbour. Reaching 253 from zero at 1,000 IDs a second takes about 285,000 years, and at a million a second about 285. A database sequence that began at 1 is not the problem; one that was seeded with a large offset, or made from a timestamp, can be.

What to do about it

Limits of this measurement

The IDs are simulated. We used the Twitter-style layout with our own generator, workers numbered 0 to 9 and sequence numbers that restart each millisecond; a real service with a different layout, a different epoch or a generator that spreads its sequence differently will land on different percentages, though not on a different mechanism. Requests were spread at random, which is the friendliest assumption for a busy service, since real traffic comes in bursts. Only Chrome 153 was tested, on 26 September 2026. The Python cross-check confirms the counting, not the layout. The raw results are in the fixtures of our test suite, and the script that produced them reruns in a few seconds.

Tools used in this guide