Cracking the Code: JS, BigInt, and the Art of Future-Proofing Your App

Victor Borisov
The Glovo Tech Blog
11 min readNov 29, 2023

What’s this article about?

Ever wondered about the mysteries of JSON.parse and why it sometimes throws unexpected surprises? In the real world where we have real traffic and real users, it turns out not every platform plays nice with JSON in the same way. Parsing numbers is just the tip of the iceberg — we’ve got to tackle the quirks of both nodeJS and browsers, as well as some server-side rendering frameworks.

Join us in this exploration where we spill the beans on real-world scenarios from Glovo, sharing insights on the nitty-gritty of JSON and big numbers parsing that you might not have seen coming.

How does JS handle numbers and what is BigInt?

Historically, numbers in JS are represented using `number` type, which is based on the IEEE 754 floating-point standard, which basically means that for every `number` used there is a 64-bit double precision number in the memory, which can safely represent any integer number between -9007199254740991 and 9007199254740991 (this is Number.MAX_SAFE_INTEGER in JS), and can even work with the floating point (although with some limitations). This should be enough to cover most of the cases, but it still has some limitations, most notably when it comes to arithmetics with the floating point and storing numbers outside of the safe numbers interval.

Due to the limitations, number can not safely represent big numbers, do arithmetics with them, and also has issues with arithmetics of some floating point numbers. We can use BigInt type in JS instead of the `number`, it allows only to work with integers, but it can safely work with much bigger numbers and has a pretty solid browser support. This is all we need to know about how JS works with numbers in terms of this article, but if you’d like to dig a bit deeper into the topic, check this article out.

After reading this, you might think: “Oh, I am not planning to do any arithmetics or to work with bigger numbers, so I should not care about what you’ll say next”. And I want to assure you, we thought exactly the same until the thing that I will tell you about next happened.

The Unexpected Twist: How Neglecting BigInt Almost Broke Our App (And why this can easily happen with you)

Even though we use numbers for IDs of products in Glovo, we didn’t consider using BigInts because the numbers were very far from MAX_SAFE_INTEGER, and we were sure we would not reach this limit during the lifetime of the app. I’d personally prefer never using numbers and would go for strings instead (then we would have not had this problem in the first place), but the API was designed with more focus on the mobile apps at that time, and the issues we’ll be talking about here do not exist on major mobile platforms for native apps.

At some point, we had to plan for migrating one of our API services to a new API, which unifies several different applications, and it actually uses IDs that are higher than MAX_SAFE_INTEGER. We’ve immediately figured out this won’t work. Without doing any changes to our code, we set up our testing environment to use the new API to see how bad things are — and indeed they were really bad. The app had a bunch of errors, and we had a store which had all its products with IDs that are big enough to be coerced by the `number` limitations, meaning that after parsing the JSON, every product had exactly the same ID. To understand how this happens, see the screenshot below or check this repo with the demo app from which this screenshot is.

The result was that, when adding 1 product on the store page, all the products of this store were added to the cart, because their (originally distinct) IDs were coerced to the same number.

This happens because there are “unsafe” big numbers in the JSON response, but native `JSON.parse` has no idea what a BigInt is and there is no way to make the native JSON.parse correctly parse these numbers from the original JSON. To make the matter worse, the RFC describing the JSON standard, recommends to use numbers inside the range of the double precision numbers (same as JS’s `number`), but it does NOT enforce any specific limit and states those limits are up to each implementation (you can see it here https://datatracker.ietf.org/doc/html/rfc8259#section-6).

So the problem here is that the service returns a technically valid JSON, but we can not parse it using built-in JS tools. This means that, if you are developing a webapp, it doesn’t matter what your FE framework/library is — there is a non-zero chance that one of the APIs you depend on may start returning unsafe numbers in the JSON, because first it’s not forbidden as per JSON RFC, second many other programming languages do not have a “default” type of the number when it comes to serialising/deserialising JSON.

For instance, your Java backend may cast longer numbers to Java `Long` data type, which will not be compatible with JS `number`, and nobody may even notice any issue until that limit is breached (e.g. IDs that are being incremented in the database). For example this code:

Will produce the following JSON (it is not possible to correctly parse it with JSON.parse, if you copy-paste it to the console you will see the result):

{“id”:9223372036854775807}

This can be solved by parsing JSON without using JSON.parse and handling big numbers as BigInts. There is a library for that (tm) here: https://github.com/sidorares/json-bigint. It should cover most of the cases, but I wouldn’t be writing this article if it was the end of our struggle.

Generally, if your app is 100% client-side (SPA) and has no nodeJS server runtime, for instance something like expressJS, nuxt or next, then you should be fine with just making sure you are always using a JSON parser that can parse bigger numbers, like the one I just mentioned.

For us, it was not the end of the story. We have a customer web app with server-side-rendering (meaning we render the HTML out of VueJS components using nodeJS). It is using nodeJS and a server-side rendering framework called nuxtJS.

Behind the Server Curtain: Tackling BigInt Issues in NodeJS SSR Apps

On the server side, the problem is generally the same, with the main difference being that is (generally) we now want to parse much more JSONs, non-stop. In order to render (almost) any page, we need to make several HTTP requests to some of our backend services from the nodeJS (nuxtJS) app — even with HTTP caching that we have, this is still needed due to different customisations (for different users, cities, languages, stores, time of the day, etc), the experimentation which we do a lot (things like A/B tests) and of course almost real-time nature of our data (stores open and close, they edit products, start or finish promotions, etc).

When testing the application locally, everything works well with json-bigint parsing every backend API network response on the nodeJS server. By contrast in a real life scenario of a user navigating our website, their browser will also only parse a few JSON strings per minute (usually per page). In these “light” scenarios the json-bigint library works well. But in reality, each of our production nodeJS servers can be parsing tens or hundreds of JSONs every second, non-stop, 24/7. I am not entirely sure if there is a memory leak in the mentioned library or it’s just generally more memory-hungry, but the reality is that we didn’t find a way to use it on the server without causing a significant performance degradation caused by excessive memory used when enabling this library. We kept it as-is on the client, but we needed to find a way to parse JSONs with BigInts on the server, and, for our use-case it must be something less hungry in terms of memory.

We could not find any alternative solution for our case on the web, we were looking for something relatively popular and supported, slim so it won’t inflate our JS bundle, or at least something simple so that we could fork it and support. Most of the solutions are either too heavy in terms of the bundle size, or are not tested well enough and may fail on a valid JSON, here’s one example I found. After losing all hope, we finally managed to come up with a solution that works (well, kinda, more about that later).

What we did was take the original JSON string, at first, without parsing it. First, we ran a regular expression on it, this regex will replace all the bigger numbers with a string which contains a predefined prefix and the number itself right after it. In our case it would transform this JSON:

{“id”:9223372036854775807}

Into something like

{“id”:”APP_SERIALISED_BIGINT::9223372036854775807”}

The tricky part here is the security and reliability, because manipulating JSON strings (especially with some constants that you later transform) might be dangerous. To make this approach safer we need to make sure that what we replace is actually a big number, but not something that just looks like one. For instance, if implemented poorly, there could be a number of ways to break it:

  • If the original JSON already includes the “prefix” constant (APP_SERIALISED_BIGINT::) somewhere — this may easily break the app if there is no actual number after it. Among other risks, if the constant is placed into a JSON key, this can lead to many potential ways of allowing an attacker to manipulate the JSON structure.
  • Big numbers inside strings, especially if the JSON has a string property, which is another JSON — the function may start replacing values inside, which can break the JSON structure because it won’t work with escaped chars ( this behaviour is not desirable since the underlying JSON should be separately parsed with the same BigInt-enabled parser function).

A well-tested regular expression should help here.

After this we can take this string and feed it into the native JSON.parse. The second argument of the .parse method is called `reviver`, it is a function which is called for every value when parsing a JSON string, it allows modifying each value found in the JSON object. The code for this would look like this:

With this approach we do a (relatively) cheap in terms of memory operation of replacing some things in the string and we use the native JSON.parse, which in theory should be more efficient than any JSON parser made with JS. The only problem with this approach is that, in order to properly detect where the big number is (to avoid cases mentioned above, like JSON values inside strings), we need to use a regular expression with a negative lookbehind assertion, which has a very limited browser support, most notably it needs both macOS and iOS Safari to be of a version not less than 16.4. Luckily, it’s been supported on nodeJS since 8.10.0, so this will still work well on the server. For the browsers (mostly because of Safari) we had to still ship the custom JSON parser based on json-bigint.

The source code of the regex-based parser we created is available on github at https://github.com/vd3v/big-jason, as well as the npm package at https://www.npmjs.com/package/big-jason.

We did a performance test where we parsed a large JSON string containing different types of data, including some big numbers, using both the method with regex and the json-bigint library thousands of times. The results show that the approach with the regex consumes way less memory, around 70% less of both heap and rss memory, but it is more CPU-intensive, so it takes around 40% more time. We had no other choice but to see how much of a tradeoff would that be in production. As a result, there was no noticeable change in the memory consumption compared to what was before (where we were just losing big numbers while using the default JSON.parse), and the CPU didn’t show much change in the average load either. Before, when we were trying to use the json-bigint library in production, our servers had what we call a “slow memory leak”, where the memory would grow over the time without being cleared up, until K8S started killing those machines that ran out of memory. Under high traffic a server would not work more than a minute or two, which rendered this approach unusable for our application.

Now that we found a way to parse JSON strings with BigInt, we should be good to go, right?

Right… Not exactly. Since we are talking about an application with server side rendering, all major SSR frameworks (like next, nuxt and svelteKit) have a process called “hydration”. This is something that happens when the browser loads a page pre-rendered on nodejs, and then needs to instantiate UI components of the UI framework (like react, vue or svelte), and they need to have the same state as they had when they were rendered by the server, otherwise the user will see a page with all the data and then (once the UI library is mounted) it will become empty (it can produce much more issues, including completely breaking the app). To make it work these SSR frameworks have their ways to serialize the state, which will later be read on the client and injected into the components state before they are mounted.

As you might’ve guessed, this is exactly where the next problem with BigInts happened. Luckily, as of today this is not that much of an issue for fresh versions of both nuxt and nextjs, since they’ve released an update. They both rely on the devalue library, which got the support for BigInts in August 2022, so there is still a chance that you’re using an outdated version without BigInt support. In our case the problem was that we were stuck with nuxt2, which relies on its own fork of devalue, which still doesn’t support BigInt. On top of that, we were using @nuxtjs/composition-api, which also handles some logic related to serialization and is using good old JSON.stringify. As a quick fix, we made a patch to those dependencies in our project with pnpm patch, but I will also open PRs to those repos with a patch to potentially help other developers struggling with BigInt on old nuxt.

Conclusion

With the recent patching of server-side rendering (SSR) libraries like Nuxt and Next to support BigInt, a brighter future appears on the horizon for BigInt in JavaScript. However, the true problem lies in the JSON standard itself, as there are different ways to understand and implement it in terms of parsing numbers. The lack of native support for BigInt in JSON parsing poses an ongoing challenge by forcing developers to use 3rd party solutions for such a core thing like parsing a network response. The hope is that major browsers will collectively embrace a more inclusive approach, supporting numbers of all sizes within JSON, or there will be established a new standard for JSON that would allow representing big numbers, for instance with the JS BigInt literal (for example: { “id”: 123456789123456789n}). This would allow developers to use BigInts seamlessly across both web applications and on native mobile apps, not to mention cross-service communications.

I hope it was useful or interesting to you to read about our struggles. Please share your thoughts, questions, or even your own solutions in the comments. I am curious to hear how you handle BigInts in your apps (or if you are intentionally trying to avoid them) and your experience with this. Do you think this should become a part of the ECMA/JSON specification? Thank you very much for reading and have a nice day!

--

--