cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

BigInt inside the engine

Learn how V8 stores BigInts as heap digit arrays, chooses arithmetic algorithms, converts strings, handles 64-bit paths, and enforces size limits.

By the end, you can
  • 01
    Read a V8 BigInt layoutExplain sign, length, little-endian 64-bit digits, zero length, and why V8 has no Smi-like BigInt immediate.
  • 02
    Connect operations to costsTrace digit addition, one Karatsuba split, V8 multiplication thresholds, string conversion strategies, and division choices.
  • 03
    Use BigInt deliberatelyChoose Number, BigInt, BigInt64Array, and fixed-width wrapping without making timing claims or mixing numeric types accidentally.

BigInts are heap digit arrays

A JavaScript BigInt is the language's exact integer value. Inside V8 12.4, the value is represented by a heap object whose payload is a sign plus an array of machine-word digits. The V8 blog's original BigInt article describes the same idea as storing a large integer in register-sized chunks called digits, with the sign stored separately.

Definition

In V8, a BigInt is a BIG_INT_BASE_TYPE heap object with a sign bit, a digit length, and little-endian digits. Small Numbers can be Smis, but V8 has no Smi-like immediate form for BigInt: even 1n is a BigInt heap object.

This lesson builds on the beginner BigInt lesson and Tagged values, Smis & heap numbers. We focus on storage and costs, not syntax: why arithmetic creates new immutable results, why multiplication changes algorithms, why decimal strings are work, and how fixed-width 64-bit APIs fit into the picture.

Real-life analogyA long number written across boxes on a form

Write a long number across several boxes on a form. The sign, the number of boxes, and the contents of each box are separate. Changing the number means making a fresh result instead of changing the old one.

In real life: A plus or minus box
In JavaScript: V8's sign bit
In real life: The count of number boxes used
In JavaScript: The BigInt digit length
In real life: Each box holds one fixed-size part
In JavaScript: One 64-bit digit on this Node/V8 build
In real life: A changed number uses a fresh form
In JavaScript: BigInt arithmetic returns a new immutable value

Where the analogy stops: A paper form uses decimal digits. V8 stores binary machine words and can switch algorithms for very large values.

Sign, length, and 64-bit digits

The primary source is V8's src/objects/bigint.h. BigIntBase stores sign and length in one atomic bitfield, defines digit_t = uintptr_t, and exposes a flexible digit array. On the Node 22 build used by the tests, uintptr_t is 64 bits, so 2n ** 64n prints two digits: 0x0 then 0x1.

V8 BigInt layout pieces
PieceVerified factWhat it means
MapBIG_INT_BASE_TYPE; %DebugPrint shows the map itself in ReadOnlySpace.All ordinary BigInt values point at this heap-object shape in V8.
BitfieldOne atomic bitfield stores the sign bit and a 30-bit length.sign: 1 means negative; length: 0 represents 0n.
DigitsA flexible array of uintptr_t digits, so Node's non-pointer-compressed 64-bit build uses 64-bit digits.Digits are little-endian: 2n ** 64n prints 0x0, then 0x1.
Node-only proof with %DebugPrintJavaScript
// Node-only: run with --allow-natives-syntax.for (const [label, value] of [  ["ten", 10n],  ["two64", 2n ** 64n],  ["negative", -5n],  ["zero", 0n],]) {  process.stdout.write("@@" + label + "\n");  %DebugPrint(value);}

The test asserts only stable text: [BigInt], BIG_INT_BASE_TYPE, instance size: variable, length, sign, and digit lines. It does not assert addresses or exact object sizes. Node 22 here uses V8 12.4 without pointer compression; Chrome uses pointer compression, the sandbox, and a newer V8, so address-looking details differ.

Playground: decode a BigInt into 64-bit digits
Browser-safe digit extractionPop out in the code editor (opens in a new tab)JavaScript
const value = 2n ** 64n;const mask = (1n << 64n) - 1n;const sign = value < 0n ? "negative" : "positive";const lowDigit = value & mask;const nextDigit = (value >> 64n) & mask; console.log(sign);console.log("0x" + lowDigit.toString(16));console.log("0x" + nextDigit.toString(16));
Decoded layout18446744073709551616n
sign bit0
length2
object size model40 bytes

64-bit digits, least significant first

  1. digit 00x0
  2. digit 10x1
Try it yourself

18446744073709551616n has sign 0, length 2, and digits 0x0, 0x1. The byte count is a labelled teaching model, not a real object-size probe.

The digit list is computed with real BigInt & and >> operations. %DebugPrint in the tests proves the same little-endian digits for Node/V8 12.4.

The playground computes the same little-endian digits with real BigInt operations: mask with 0xffffffffffffffffn, then shift by 64n. Its byte count is labelled as a teaching model so it does not pretend to be a heap inspector.

BigInt keys compare by valuePop out in the code editor (opens in a new tab)JavaScript
const cache = new Map([[1n, "a"]]);const set = new Set([1n, BigInt("1")]); console.log(1n === 1n);console.log(cache.get(1n));console.log(set.size);

1n === 1n compares the BigInt value. The Map lookup and the Set size show that BigInt keys use value hashing, not heap-object identity.

Addition is digit-by-digit

Addition and subtraction walk the digit arrays with carries or borrows. A one-digit add is cheap; a thousand-digit add touches roughly a thousand digits. V8 creates a fresh BigInt result because BigInt values are immutable. The public value is primitive, but the internal result storage still has to be allocated unless the compiler can avoid it in a narrower optimized path.

Step through 64-bit-digit addition
Step 0 of 14Ready
Your turn: follow the blue line

Step through schoolbook BigInt addition: add each 64-bit digit, keep low bits, carry high bits, then compare with native BigInt.

Running in
  1. script
Next: line 25
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const BIGINT_BASE = 1n << 64n;const DIGIT_MASK = BIGINT_BASE - 1n; function fromDigits(digits) {  let value = 0n;  for (let index = digits.length - 1; index >= 0; index -= 1) {    value = value * BIGINT_BASE + digits[index];  }  return value;} function addDigits(left, right) {  const out = [];  let carry = 0n;  const length = Math.max(left.length, right.length);  for (let index = 0; index < length; index += 1) {    const sum = (left[index] ?? 0n) + (right[index] ?? 0n) + carry;    out[index] = sum & DIGIT_MASK;    carry = sum >> 64n;  }  if (carry) out.push(carry);  return out;} const right = [0x2n];const digits = addDigits(left, right);console.log(digits.map((digit) => "0x" + digit.toString(16)).join(", "));console.log(fromDigits(digits) === fromDigits(left) + fromDigits(right));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Line 17 in the walkthrough is the whole trick: add one 64-bit lane from the left, one from the right, and the carry. Line 18 stores the low 64 bits, and line 19 carries the overflow into the next digit. The final line checks the teaching implementation against native BigInt addition.

Multiplication algorithms

V8's bigint-internal.h sets the thresholds. The operation dispatcher in bigint-internal.cc chooses based on the shorter operand's digit count. The 2018 V8 BigInt blog explains the original schoolbook implementation; V8 12.4 also ships Karatsuba, Toom-Cook-3, and FFT files.

Thresholds fetched from V8 12.4.254.21Text
V8 12.4.254.21 src/bigint/bigint-internal.h:kKaratsubaThreshold = 34 digitskToomThreshold      = 193 digitskFftThreshold       = 1500 digitskFftInnerThreshold  = 200 digitskBurnikelThreshold  = 57 digitskBarrettThreshold   = 13310 digitskToStringFastThreshold = 43 digitskFromStringLargeThreshold = 300 digits
V8 multiplication thresholds by digit count
AlgorithmRangeCost modelPrimary source
single digitshorter operand length is 1O(n)MultiplySingle in src/bigint/bigint-internal.cc
schoolbook2 through 33 digitsO(n²)kKaratsubaThreshold = 34; mul-schoolbook.cc
Karatsuba34 through 192 digitsabout O(n^1.585)kToomThreshold = 193; mul-karatsuba.cc
Toom-Cook-3193 through 1499 digitsabout O(n^1.465)kFftThreshold = 1500; mul-toom.cc
FFT1500 digits and aboveSchönhage-Strassen-style sub-quadraticmul-fft.cc; advanced algorithms are enabled by default except Android

The FFT file says the algorithm is due to Schönhage and Strassen. The build flag v8_advanced_bigint_algorithms is enabled by default except on Android; without it, the source falls back to Karatsuba for larger products.

Step through one Karatsuba split
Step 0 of 11Ready
Your turn: follow the blue line

Step through one Karatsuba split: split, compute z0, z2, one cross term, then recombine and check the real product.

Running in
  1. script
Next: line 14
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const SPLIT_BASE = 1000n; function karatsubaOneLevel(x, y) {  const xLow = x % SPLIT_BASE;  const xHigh = x / SPLIT_BASE;  const yLow = y % SPLIT_BASE;  const yHigh = y / SPLIT_BASE;  const z0 = xLow * yLow;  const z2 = xHigh * yHigh;  const z1 = (xLow + xHigh) * (yLow + yHigh) - z2 - z0;  return z2 * SPLIT_BASE * SPLIT_BASE + z1 * SPLIT_BASE + z0;} console.log(product === 123456n * 789012n);console.log(product.toString());
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Karatsuba saves work by replacing two cross multiplications with one product of sums and two subtractions. The walkthrough uses base 1000n so the split is readable; V8 splits in machine-word digits.

V8 division paths verified in src/bigint
PathWhen it appearsWhat to remember
single digitdivisor length is 1Divide by one machine-word digit.
schoolbook / Knuthdivisor length below 57kBurnikelThreshold = 57; V8's source says the schoolbook path is loosely based on Go/Knuth.
Burnikel-Zieglerlarge divisor, or Barrett not selectedRecursive division from div-burnikel.cc.
Barrettadvanced algorithms, divisor length at least 13310, and dividend longer than divisorkBarrettThreshold = 13310; implemented in div-barrett.cc.
Which multiplication path starts?
  • 10-digit operands
  • 33-digit operands
  • 34-digit operands
  • 192-digit operands
  • 193-digit operands
  • 1499-digit operands
  • 1500-digit operands
Try it yourself
0 of 7 correct

Sort each shorter-operand digit count by the V8 12.4 thresholds.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Converting to and from strings

BigInt values store binary digits, not decimal text. tostring.cc uses a special base-power-of-two path for radices like 16, and a divide-and-conquer path for large non-power radices. fromstring.cc parses large inputs by combining neighboring chunks so fast multiplication can help.

String conversion paths in V8's BigInt library
CaseAlgorithmWhy it matters
Power-of-two radixUse bit grouping for bases such as 2, 8, 16, and 32.No repeated decimal division is needed.
Small non-power radixClassic multiply/divide by chunks.Good for shorter values.
Large toStringV8 switches at kToStringFastThreshold = 43 digits.The fast path recursively divides by half-size powers and uses Barrett division.
Large parseV8 switches at kFromStringLargeThreshold = 300 result digits.It combines neighboring chunks so fast multiplication algorithms can help.
Round-trip decimal text through hexadecimalPop out in the code editor (opens in a new tab)JavaScript
const decimal = "900719925474099312345";const id = BigInt(decimal);const hex = id.toString(16); console.log(hex);console.log(BigInt("0x" + hex) === id);

Printing a huge decimal BigInt is expensive because the engine must convert from base 2**64 digits to base 10 characters. That is useful work, not a cached property read.

BigInt64 fast paths and typed arrays

Fixed-width APIs are different from arbitrary-precision BigInt arithmetic. BigInt.asIntN(64, value) and BigInt.asUintN(64, value) keep the low 64 bits. BigInt64Array and BigUint64Array store raw 64-bit lanes in an ArrayBuffer; reading an element gives JavaScript a BigInt value.

Raw bytes behind a BigInt64ArrayPop out in the code editor (opens in a new tab)JavaScript
const buffer = new ArrayBuffer(16);const signed = new BigInt64Array(buffer);const bytes = new Uint8Array(buffer); signed[0] = -1n;signed[1] = (1n << 63n) - 1n; console.log(signed[0]);console.log(bytes.slice(0, 8).every((byte) => byte === 255));console.log(BigInt.asUintN(64, -1n));
What V8 optimizes

V8 12.4's type-hints.h includes kBigInt and kBigInt64. Compiler sources such as simplified-lowering.cc lower checked BigInt64 operations to word-64 machine representations in the right contexts. This is a representation fact, not a promise that every BigInt expression is faster.

The byte view proves the buffer holds raw bytes: writing -1n to a signed lane fills the first eight bytes with 255. The read still returns a BigInt primitive, not a Number.

Maximum BigInt size

BigInt means arbitrary precision, not infinite precision. V8's BigInt::kMaxLengthBits is 1 << 30, about one billion bits. That value is in src/objects/bigint.h, beside the note that V8 chooses a platform-independent limit below the implementation maximum.

Guarded maximum-size probeJavaScript
// Node-only guard: V8 rejects the result length before allocating the value.try {  2n ** (2n ** 30n);} catch (error) {  console.log(error.name + ": " + error.message);}
Verified error

The lesson test runs the probe in a child process and asserts RangeError: Maximum BigInt size exceeded. The exponent is rejected by V8's size guard quickly; the test does not try to allocate a billion-bit result.

Other engine layouts

The language rules are shared by the ECMAScript BigInt specification, but engines can choose different private storage. These source snapshots are useful comparisons, not portability hooks.

BigInt storage choices across engines
EngineStorage choiceVerified source fact
V8 12.4Heap object for every BigInt valueSign/length bitfield plus machine-word digits; no Smi-like BigInt immediate.
JavaScriptCoreOptional BigInt32 immediate exists in sourceUSE(BIGINT32) adds a BigInt32Tag, but WebKit currently defines USE_BIGINT32 0.
SpiderMonkeyHeap cell with inline digitsJS::BigInt stores small digit arrays inline before switching to heap digit storage.

JavaScriptCore's JSCJSValue.h has EncodeAsBigInt32 and a BigInt32Tag under USE(BIGINT32), but WebKit's platform header currently defines USE_BIGINT32 0. SpiderMonkey's BigIntType.h defines InlineDigitsLength and uses inline digits until the value needs heap digit storage.

Practical use

Use Number when values fit inside Number.MAX_SAFE_INTEGER, when decimals matter, or when you need Math APIs. Use BigInt for exact large integers: database IDs, cryptographic math, bit masks, counters that cross 53 bits, and 64-bit protocol fields.

Bound a protocol field to 64 bitsPop out in the code editor (opens in a new tab)JavaScript
const fromProtocol = BigInt.asUintN(64, 18446744073709551616n + 5n);const smallCount = 42n; console.log(fromProtocol);console.log(Number(smallCount));
  • Keep Number and BigInt arithmetic separate; convert deliberately at boundaries.
  • Use BigInt.asUintN(64, value) or BigInt.asIntN(64, value) when the domain is fixed-width.
  • Do not convert a large decimal ID through Number first; parse the string with BigInt(text).
  • Measure hot paths. Algorithm thresholds and BigInt64 lowering are engine facts, not universal timing claims.

Common misconceptions

  • “Small BigInts are immediate values.” Not in V8 12.4. The tests prove 10n is a [BigInt] heap object.
  • “BigInt has no maximum.” V8 sets a maximum bit length and throws a RangeError when the result would exceed it.
  • “BigInt64Array stores BigInt objects in the buffer.” The buffer stores raw 64-bit lanes; reads produce BigInt values.
  • “Decimal printing is free.” Converting binary digits to decimal characters is an algorithm with real cost.
Similar-looking BigInt ideas
IdeaAccurate statementCommon trap
BigInt value equality1n === 1n compares numeric BigInt values.It is not object identity; Map and Set use value hashing for BigInt keys.
Heap allocationV8 BigInt values are heap objects and arithmetic creates immutable results.That does not mean every operation is slow enough to matter in real programs.
BigInt64ArrayThe array stores raw 64-bit lanes in an ArrayBuffer.Reading an element produces a BigInt value; the element is not a heap BigInt sitting inside the buffer.
Maximum sizeV8 rejects values above 1 << 30 bits.BigInt is arbitrary precision within implementation and memory limits, not mathematical infinity.
Real-life analogyTwo forms with the same number

Two forms can both show the number 1. When you need the value, the number matters, not which sheet it is on. BigInt keys work that way in Map and Set.

In real life: Two forms both show 1
In JavaScript: Two expressions can produce the same 1n value
In real life: You compare the written number
In JavaScript: Map and Set use BigInt value equality
In real life: A changed total goes on a new form
In JavaScript: Arithmetic creates a new immutable result

Where the analogy stops: Paper forms have separate physical identities. BigInt primitives compare by numeric value, so two ways of producing 1n are the same Map or Set key.

Practice exercises

Exercise 1 · Warm-upDecode 2n ** 64n

Run the program and type the two digit strings in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const value = 2n ** 64n;
const mask = (1n << 64n) - 1n;
console.log("0x" + (value & mask).toString(16));
console.log("0x" + ((value >> 64n) & mask).toString(16));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeProve BigInt map keys use value equality

    Predict the value returned by the Map lookup.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cache = new Map([[1n, "a"]]);
    console.log(cache.get(BigInt("1")));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCarry one 64-bit digit

      Write the low digit and the carry produced by the addition.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const mask = (1n << 64n) - 1n;
      const sum = 0xffffffffffffffffn + 2n;
      console.log("0x" + (sum & mask).toString(16));
      console.log(sum >> 64n);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeCheck the Karatsuba example

        Run the multiplication and type the decimal product.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        console.log((123456n * 789012n).toString());

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeWrap negative one as unsigned 64-bit

          Predict the unsigned wrapped value.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log(BigInt.asUintN(64, -1n));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeKeep a counter bounded

            Use the 64-bit wrapping rule to predict the remaining value.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            console.log(BigInt.asUintN(64, 18446744073709551616n + 5n));

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz

              BigInt internals quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is a V8 BigInt in this lesson's verified Node/V8 build?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does V8 print for 0n's digit length?

                Choose an answer to see the explanation.

              3. Question 3 of 8What does this BigInt key code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const cache = new Map([[1n, "a"]]);
                const set = new Set([1n, BigInt("1")]);
                console.log(1n === 1n);
                console.log(cache.get(1n));
                console.log(set.size);

                Choose an answer to see the explanation.

              4. Question 4 of 8At what shorter-operand digit length does V8 12.4 switch from schoolbook to Karatsuba multiplication?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this fixed-width BigInt code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const buffer = new ArrayBuffer(8);
                const signed = new BigInt64Array(buffer);
                signed[0] = -1n;
                console.log(signed[0]);
                console.log(BigInt.asUintN(64, signed[0]));

                Choose an answer to see the explanation.

              6. Question 6 of 8Why can big.toString(10) be expensive for a huge BigInt?

                Choose an answer to see the explanation.

              7. Question 7 of 8What error does V8 throw for 2n ** (2n ** 30n)?

                Choose an answer to see the explanation.

              8. Question 8 of 8What is the practical rule for 64-bit protocol fields?

                Choose an answer to see the explanation.

              Key takeaways

              • V8 BigInts are heap objects with a sign, a length, and little-endian machine-word digits.
              • Zero has length zero; 2n ** 64n has digits 0x0 and 0x1 on the verified 64-bit build.
              • Addition is linear in digit count; multiplication switches from schoolbook to Karatsuba, Toom-Cook, and FFT at verified thresholds.
              • String conversion and parsing are real algorithms, especially for large decimal inputs.
              • Use BigInt for exact large integers, and use asUintN(64)/asIntN(64) for fixed-width lanes.

              One-line definition: A BigInt is an exact integer value that V8 stores as an immutable heap object containing sign, length, and machine-word digits.

              Up next: Symbols inside the engine.

              CompleteFrontend Clear concepts. Working examples.