The Missing Cent
Google's own billing query prints $1005.004832999999984 in one column and $1005.00 in the next. The gap is not the float. It is the place where you rounded — and the two failures are separated by ten orders of magnitude
Published: 2026-11-01 | jslet Research | 17 min read
馃搼 In This Briefing
- Three Errors, One Decimal Point
- Seventeen Digits, Fifteen Trusted
- Why a Tenth Is Not a Tenth
- The Rounding That Is Not There
- What a Million Rows Do
- The Error That Costs Money
- A Unit That Is Really a Type
- The Wall at 9,007,199,254,740,992
- The Identifier That Changes
- Not Every Currency Has a Cent
- Exact Is Not the Same as Safe
- This Site Runs the Same Arithmetic
- Frequently Asked Questions
- Methodology & Disclosure
There is a query in Google Cloud's billing documentation that returns the total cost on an invoice. It returns two columns. The first sums the cost field as it is stored. The second converts every value to micros, sums the integers, and divides at the end.
The documentation then prints what the two columns look like on real exported data:
| month | total | total_exact |
|---|---|---|
| 201901 | $1005.004832999999984 | $1005.00 |
| 201901 | $781.8499760000028 | $781.85 |
| 201902 | $953.0034923645475983 | $953.03 |
| 201902 | $992.3101739999999717 | $992.31 |
| 201902 | $1.2817819999999998 | $1.28 |
This is not an illustration I constructed. It is the vendor's own documentation, describing its own export format, and the vendor says plainly why the two columns differ: "The total field directly sums the floating point cost and credit values, which can result in floating point rounding errors. The total_exact field converts costs and credit values to micros before summing, then converts back to dollars after summing, avoiding the floating point rounding error."
Five values, five trailing tails. The fifth is the one worth staring at: $1.2817819999999998. The true amount is a cent and change. The stored sum has sixteen more digits, and every one of them is noise.
Most writing about this topic stops there and says: floating point is bad, use integers. That is the right conclusion attached to the wrong reason, and it hides a much more expensive failure that no amount of integer arithmetic will fix.
1. Three Errors, One Decimal Point
When a total disagrees with an expectation by a cent, three unrelated things could be responsible, and they look identical on a screen. They are not comparable in size. Sorted by magnitude, they span the entire range from "invisible in any currency" to "the whole invoice".
| Failure | Typical size | Trigger | What you see |
|---|---|---|---|
| Representation | ~1e-17 relative | Any fraction whose denominator has a prime factor other than 2 | Equality fails; 17 digits leak into output |
| Accumulation | ~1e-11 relative per million adds | Summing many values in sequence | The tails in the table above |
| Rounding | 0 to 100% of a line item | Deciding where to round, not how | A total that is simply wrong |
The first two are properties of binary floating point. The third is not. Rounding error survives in exact decimal arithmetic, in a ledger written by hand, and in a database column declared DECIMAL(18,6). It has nothing to do with the mantissa.
That distinction is the whole reason this article exists. The literature, the conference talks and the Stack Overflow answers are almost entirely about the first two, which are the two that do not move money. The one that does is a design decision about ordering, and it is invisible in every "0.1 + 0.2" demo on the internet.
2. Seventeen Digits, Fifteen You Can Trust
IEEE 754 binary64 — the format behind JavaScript's Number, Python's float, Java's double, and the FLOAT64 column type in BigQuery — spends 64 bits as one sign bit, eleven exponent bits, and a 52-bit fraction with an implicit leading one. That is 53 bits of significand.
Converting binary accuracy to decimal digits is one multiplication: 53 × log10(2) = 15.9546. Not 15, not 16, not 17. The fractional part is the entire source of the folklore, because a non-integer digit count has to be rounded in one direction or the other depending on what you are promising.
| Claim | Digits | What it means |
|---|---|---|
| Guaranteed round trip | 15 | Any 15-digit decimal survives to binary64 and back unchanged |
| Guaranteed distinction | 17 | 17 digits are always enough to tell two adjacent doubles apart |
Between 15 and 17 there is a zone where the answer depends on the value, which is exactly where bugs live. Here is the same expression printed at three precisions:
| Significant digits | Output of 0.1 + 0.2 |
|---|---|
| 15 | 0.3 |
| 16 | 0.3 |
| 17 | 0.30000000000000004 |
One value, three renderings, and the first two are the ones your logging pipeline uses. This is not a curiosity. It explains the single most common question in this domain — "the code looks right, why does the test fail" — because the log line and the assertion are looking at the same 64 bits and reporting different stories.
The precision of the format is not optional and cannot be tuned. What can be chosen is how many of the digits you print, and the two choices are independent: a system can be exact to fifteen digits and still rounded to two for display, which is the normal case and also the case where a false sense of safety comes from.
3. Why a Tenth Is Not a Tenth
A binary64 value is m × 2e. Every representable number is therefore a dyadic rational: a fraction whose denominator is a power of two. One half, one quarter, three eighths, and every sum of those.
One tenth has a denominator of ten, which factors as 2 × 5. The factor of five cannot be built out of twos. So the stored value is the nearest dyadic rational, and the nearest one is not very near in absolute terms:
0.1 → 0.1000000000000000055511151231257827021181583404541015625
That is 55 significant digits of stored value standing in for one digit of intent. The absolute error is 5.55e-18, and the relative error is 5.55e-17. It is the best binary64 can do at that magnitude, it is two fifths of one unit in the last place, and it is wrong.
| Decimal | Denominator | Exact in binary64 |
|---|---|---|
| 0.5 | 2 | Yes |
| 0.25 | 4 | Yes |
| 0.125 | 8 | Yes |
| 0.1 | 10 = 2 × 5 | No |
| 0.2 | 10 = 2 × 5 | No |
| 0.05 | 20 = 4 × 5 | No |
| 0.01 | 100 = 4 × 25 | No |
The rule is mechanical, which is the useful part. You do not have to remember which constants are safe; you factor the denominator and look for a five. Every cent, every half cent, every price ending in a nine — all of them carry a five, which means the entire domain of money is on the wrong side of this table.
Note what is not claimed here. The error is not growing, not drifting, and not causing visible damage at the magnitudes most calculators work in. A price of $0.10 stored as $0.1000000000000000055 will display as $0.10 at any reasonable precision. The problem is never the displayed price. It is the comparison, the sum, and the identifier — the three places where the extra bits are read.
4. The Rounding That Is Not There
Rounding to two decimals means finding the nearest multiple of one hundredth. In exact arithmetic this is well defined, including at the midpoint: 1.005 is exactly halfway between 1.00 and 1.01, and a rule has to be chosen. In binary floating point there is no midpoint to be halfway from, because 1.005 is not representable either.
What the stored value actually is, is 1.00499999999999989341858963598497211933135986328125. That is strictly less than the midpoint. There is nothing ambiguous left to resolve, and the only correct answer is 1.00.
Here are five values where that happens, run through toFixed(2):
| Literal | Stored as | toFixed(2) | Expected |
|---|---|---|---|
| 1.005 | 1.00499999999999989... | 1.00 | 1.01 |
| 8.575 | 8.57499999999999928... | 8.57 | 8.58 |
| 2.675 | 2.67499999999999982... | 2.67 | 2.68 |
| 0.615 | 0.61499999999999999... | 0.61 | 0.62 |
| 10.235 | 10.23499999999999943... | 10.23 | 10.24 |
Five for five, all in the same direction. That is not a coincidence and not a bug in any particular engine — it is what the arithmetic says. And the direction is biased: if your prices are generated as x.xx5, you will systematically under-collect, forever, by up to half a cent per line.
Now the mitigation that everyone reaches for:
Math.round(1.005 * 100) / 100 → 1
It fails for the same reason, one step earlier. 1.005 * 100 is not 100.5; it is 100.49999999999999. Multiplying introduced a second rounding, and the second one landed on the wrong side of 100.5 just as the first one landed on the wrong side of the midpoint. Multiplying by a power of ten does not move a binary number into decimal space. It moves it into a different binary number.
The two built-in rounding functions do not even agree with each other:
| Expression | Result |
|---|---|
(-1.5).toFixed(0) | "-2" |
Math.round(-1.5) | -1 |
Same runtime, same input, two answers. Math.round is defined to round toward positive infinity at the midpoint; toFixed is defined to format rather than to round, and its tie behaviour runs the other way. Neither is wrong. They are answering different questions, and both are named with the word "round".
5. What a Million Rows Do
Accumulation is the failure that gets demonstrated and never quantified. Adding a million values in sequence does not produce a million times the error of one value, because each partial sum rounds at its own magnitude. The total error is roughly the square root of the count times the per-operation error, which sounds alarming until you put units on it.
Adding 0.01 one million times, in order, gives 10000.000000171856. The exact answer is 10000. The drift is +1.72e-7 dollars, which is 0.0000172 cents. You cannot display that number in a currency format and have it mean anything.
Do it ten million times and the sign flips: 99999.99998630969, which is 1.37e-5 short. Adding more rows made the error negative. There is no correction you can apply, because the direction is a function of the exact magnitudes and the order of the additions rather than of the count.
| Operation | Result | Error | In cents |
|---|---|---|---|
| 1,000,000 × 0.01 | 10000.000000171856 | +1.72e-7 | 0.0000172 |
| 10,000,000 × 0.01 | 99999.99998630969 | -1.37e-5 | -0.00137 |
This is the honest size of accumulation error at billing scale: less than a hundredth of a cent per ten million rows. It produces the ugly trailing digits in a debugging session. It does not produce a discrepancy anyone can find in a reconciliation.
Which brings the first two failures to their real status. Representation error is 1e-17. Accumulation error over ten million rows is 1e-5 cents. Together they are a display problem, and the fix is a CAST at the boundary, which is precisely what the micros columns in the Google examples do. The reason the industry treats this as the big problem is that it is the visible one.
6. The Error That Costs Money
A bill has to be denominated in something. Suppose the smallest unit is the cent, because the invoice prints two decimals. Now take a million line items, each costing $0.0049 — less than half a cent, which is not an unusual unit price for a single API call.
| Strategy | Total | What happened to the money |
|---|---|---|
| Sum exactly, round once at the end | $4,900.00 | Collected in full |
| Round each line to cents, then sum | $0.00 | Every line rounded to zero |
Same usage data. Same data types. Two hundred lines of code, both defensible, and the revenue differs by 100 percent. Note that this is not a floating point story — run it in DECIMAL(18,6) and the result is identical, because the rounding is the point.
The half-cent threshold makes this concrete. Compute how much usage has to accumulate before a single line reaches the point where rounding to cents can see it:
| Unit price | Calls to reach $0.005 | Effect of per-line rounding |
|---|---|---|
| $0.0000002 per call | 25,000 | Customers below 25,000 calls per line pay nothing |
| $0.000001 per call | 5,000 | Customers below 5,000 calls per line pay nothing |
This is why cloud billing systems do not round at the line. Serverless pricing is deliberately denominated below the cent, and the invoice is an aggregate of hundreds of thousands of sub-cent quantities. Rounding each of them to the currency's smallest printed unit is not a rounding policy; it is a discount.
The reverse error costs the customer instead, and it is bounded differently. If you round each line up, the maximum overcharge is half a cent per line, so a million lines can be billed $5,000 more than the usage justifies. That is the number that makes this a real failure rather than a curiosity: the worst case of per-line rounding is $5,000 per million rows, and the worst case of floating point accumulation is $0.0000002.
There is a tell in the vendor documentation. Google's billing export schema has a cost_type field, and the documented values are "regular costs, taxes, adjustments, and rounding errors". A rounding error is a first-class accounting category, with its own row, because the money it represents has to land somewhere. A system that buries it in a decimal point cannot produce that row, and cannot tell you which direction it went.
7. A Unit That Is Really a Type
A micro is one millionth: 1,234,567 micros is $1.234567. The name suggests a smaller ruler, as though a finer unit were the fix for a precision problem. That is the wrong reading, and the wrong reading leads people to invent micro-micros, which does not help.
The reason micros work is that they are integers. Binary64 represents every integer up to 253 exactly, with no error at all, and integer addition of two exactly represented integers produces an exactly represented integer as long as the result stays in range. There is no error to accumulate because there are no fractional bits to lose.
Compare the two representations of the same amount:
| Amount | Stored as | Exact |
|---|---|---|
| $0.000123 | double 0.000123 | No — denominator has a factor of 5 |
| $0.000123 | 123 micros | Yes — 123 is an integer below 253 |
So the conversion in the Google example is not a unit change. It is a change of domain, from the set of dyadic rationals to the set of small integers, and the second set has no representation error by construction. The CAST is doing the work that the word "micros" gets credit for.
One consequence follows immediately, and it is the useful one for anyone building a cost model: once every value is an integer, the order of addition stops mattering. Ten million micros added forward and added backward give the same integer. No Kahan summation, no sorted accumulation, no compensated arithmetic. The properties that floating point makes fragile become free.
8. The Wall at 9,007,199,254,740,992
253 is 9,007,199,254,740,992. Above it, binary64 still represents integers, but not every integer: the spacing between adjacent representable values doubles, then doubles again. The numbers stay clean-looking and stop being distinct.
The name in JavaScript is Number.MAX_SAFE_INTEGER, and it equals 253 − 1 = 9,007,199,254,740,991, one less than the power of two. The off-by-one is not arbitrary. 253 itself is exactly representable — the test Number.isInteger(Math.pow(2, 53)) returns true — but 253 + 1 is not, so a value at 253 cannot be safely incremented. "Safe" here means "n and n + 1 are distinguishable", not "n can be stored". The name describes a neighbourhood property and reads like a ceiling.
| Representation | Largest exact integer | In dollars, as micros |
|---|---|---|
| binary64 | 9,007,199,254,740,992 | $9,007,199,254.74 |
| int64 | 9,223,372,036,854,775,807 | $9,223,372,036,854.78 |
The ratio between those two ceilings is 1,024, and that is not a coincidence: it is 210, because the signed 64-bit integer carries ten more bits than the double's significand. The practical difference is between ninety billion dollars and nine trillion. One is the lifetime spend of a large customer. The other is the annual spend of the entire cloud market. Choosing the wrong column type does not fail at the first invoice; it fails at an aggregate nobody is watching.
Denominate in cents instead of micros and the double ceiling becomes $90,071,992,547.41 — ninety billion, a hundred times better and still two orders below the integer ceiling. Which is the reason the answer is not "use a bigger unit". A bigger unit buys precision in the wrong direction: it reduces how small an amount you can express, while the risk being managed is how large a total you can hold.
RFC 7493 states the boundary directly, and it is the sentence to quote at anyone who proposes a JSON number for money: "An I-JSON sender cannot expect a receiver to treat an integer whose absolute value is greater than 9007199254740991 (i.e., that is outside the range [-(2**53)+1, (2**53)-1]) as an exact value."
9. The Identifier That Changes
Money is the case everyone discusses. Identifiers are the case that catches people who already know to store money as integers, because the identifier is not money and does not look like it needs a type decision.
A 19-digit Snowflake identifier takes a predictable path through a JavaScript service:
| Stage | Value |
|---|---|
| As written by the database | 1234567890123456789 |
After JSON.parse | 1234567890123456768 |
After String() | 1234567890123456800 |
Three numbers for one object. The second is the actual IEEE value and the third is its shortest round-trip rendering, which differs from the second in the last three digits it prints. A human comparing a log line to a database row sees a 21-unit difference and calls it a data corruption. It is arithmetic.
The behaviour at both ends of the range is worth knowing:
| Input | Survives as | Delta |
|---|---|---|
| 9,007,199,254,740,993 (253 + 1) | 9,007,199,254,740,992 | −1 |
| 1,234,567,890,123,456,789 | 1,234,567,890,123,456,768 | −21 |
| 9,223,372,036,854,775,807 (int64 max) | 9,223,372,036,854,775,808 | +1 |
The last row is the interesting one. The largest signed 64-bit integer, pushed through a double and printed back, becomes larger than the maximum it came from. If that value is handed to a system that range-checks on the way in, it passes. If it is handed back to a database expecting a signed 64-bit integer, it does not fit.
And the damage does not compound. Parse and re-serialise the same document three times and the value stays at 1234567890123456800 — the first conversion is the only one that loses anything, and after that it is stable. Nothing throws, nothing appears in a diff, and round-trip tests pass, because the value being round-tripped is now the wrong one.
RFC 8259 is explicit that this is permitted rather than a defect, which is why the fix has to be at the schema level: "This specification allows implementations to set limits on the range and precision of numbers accepted." The same document then gives the interoperable window, [-(2**53)+1, (2**53)-1], and RFC 7493 recommends that anything outside it travel as a JSON string.
10. Not Every Currency Has a Cent
Everything above assumed the smallest unit is one hundredth. ISO 4217 does not. Each currency carries a minor unit exponent, and it is not always two:
| Exponent | Examples | Count |
|---|---|---|
| 0 | JPY, KRW, VND, CLP, ISK, UGX, XAF, XOF, XPF | 17–31* |
| 2 | USD, EUR, GBP, CNY, and most others | majority |
| 3 | BHD, IQD, JOD, KWD, LYD, OMR, TND | 7 |
| 4 | CLF, UYW | 2 |
*The zero-decimal count depends on which edition of the list you read and whether indexed units of account are included. The three-decimal and four-decimal groups are stable at seven and two.
The multiplication error runs in both directions and is not subtle. Treating yen as a two-decimal currency sends 300000 where 3000 was meant, and charges a hundred times the price. Treating Kuwaiti dinar as a two-decimal currency sends 1250 where 12500 was meant, and charges a tenth. One direction is a refund and a support ticket; the other is a chargeback and a compliance conversation.
Stripe's documentation is unambiguous about what the number means: "All API requests expect amount values in the currency's minor unit." It then gives both cases in one line each — 1000 for ten US dollars, 10 for ten yen.
But the exponent is not a property of the currency. It is a property of the interface. Stripe documents Icelandic kr贸na as a special case: ISK is zero-decimal in ISO 4217, yet the API requires it represented as a two-decimal value where the trailing digits are always 00. Charging 5 ISK means sending 500. A system that reads the standard, multiplies by one, and sends 5 is sending five hundredths of a kr贸na under a protocol that expects hundredths.
The practical rule is therefore not "never hardcode 100" but something slightly different: keep one exponent table per integration, seed it from ISO 4217, and override it wherever the provider's documentation overrides it. The table is configuration, not a constant, and it changes — currencies are added, redenominated, and retired on a schedule that has nothing to do with your release cycle.
11. Exact Is Not the Same as Safe
The advice to use a decimal type is correct, and it fixes a problem that the advice rarely names. In a decimal type, the value 1.005 is 1.005. The midpoint exists, so the rounding rule has something to operate on, and the result is reproducible across engines.
That is a narrower guarantee than "safe". Three things still have to be decided, and each one has a failure mode:
- Precision is finite. BigQuery's
NUMERICis 38 digits with a scale of 9;BIGNUMERICis 76.76 digits with a scale of 38. Exceed the declared precision and the insert raisesOUT_OF_RANGErather than silently truncating, which is better behaviour than floating point offers, but it is still a failure that reaches production the first time a currency with a large exponent meets a large aggregate. - The rounding mode is a decision. BigQuery documents that inserting 1.125 into a
NUMERIC(5, 2)column "rounds 1.125 half-up to 1.13". IEEE 754 arithmetic defaults to round-half-to-even, which would give 1.12. Both are exact systems and they disagree by one minor unit at exactly the values where it matters most — the halves. - The rounding point is still yours. Section 6 applies unchanged. A
DECIMALcolumn rounded per row loses exactly as much as aFLOAT64column rounded per row, because the type was never the problem.
Half-up versus half-even is worth a sentence of its own, because it is the only place where the two families genuinely differ in a way a customer can feel. Half-up rounds every tie away from zero, so a long sequence of ties drifts upward — the seller gains. Half-even rounds ties toward the even digit, so half of them go up and half go down, and the drift cancels over a large sample. Bankers use the second one for the same reason it is in the IEEE standard, and consumer pricing uses the first because 1.13 looks more like what a person expects than 1.12 does.
Neither is wrong, and neither can be called from a single global setting. The point is that "I used DECIMAL" answers the question of what 0.1 means, and leaves open the question of what happens at 0.005 — which is the question that appears on the invoice.
12. This Site Runs the Same Arithmetic
Everything above is easier to apply to someone else's billing system than to one's own calculators. So: the 194 HTML files this site published before this one contain 398 calls to toFixed() spread across 63 pages. Every one of them is JavaScript Number, which is binary64. Every one of them has the representation error described in section 3.
That is a real limitation and it is worth stating precisely rather than apologetically. What these calculators do not do is the second and third things that break: they do not sum a million rows, they do not persist a value that will be recomputed differently later, and they do not decide where a real invoice rounds. The arithmetic they perform is one multiplication and a division at magnitudes where the sixteenth digit is noise.
The display decision is separate, and this site makes it in the direction of fewer digits. A cost calculator that printed seventeen significant digits would be technically more honest and practically useless, because a reader who sees $1,234.5600000000001 concludes that the tool is broken, not that the tool is exact. Two decimals is a statement about readability, not about accuracy.
The failure this site can actually have is the one in section 4: a result that should round up and rounds down because the midpoint was never there. That is a per-value defect, not a per-sum defect, and it is testable — which is why the figures in this article were checked against both a V8 runtime and CPython rather than trusted from one of them.
13. Frequently Asked Questions
Is 0.1 + 0.2 really not 0.3?
In binary64 it is 0.30000000000000004, and 0.1 + 0.2 === 0.3 returns false. Both operands are already slightly wrong before the addition — 0.1 is stored as 0.1000000000000000055511151231257827021181583404541015625 — and the sum lands on a different representable number than the literal 0.3 does. Printing the result at 15 significant digits shows 0.3, which is why the defect survives in logs for years while assertions fail.
Should I store money as an integer?
Yes, but not because integers are more precise. It is because integers below 253 are exact, so the fractional part stops existing and there is nothing left to accumulate. The unit matters less than the type: micros stored in a double are exact to $9,007,199,254.74, while the same micros stored in an int64 reach $9.22 trillion.
Why does my cloud invoice differ from my calculation by a cent?
Almost never because of floating point. Summing a million rows costs about 1.7e-7 dollars of drift. A per-row round to the nearest cent costs up to half a cent per row, which is 5,000 dollars across a million rows. If the provider rounds each line and you sum exact values, the totals diverge at the rounding point, and no amount of extra precision reconciles them.
What is a micro, and why do billing exports use them?
A micro is one millionth of a currency unit, so 1,234,567 micros is 1.234567 dollars. Google Cloud's query examples convert cost to micros before summing specifically because summing raw FLOAT64 values produces trailing digits — the documented sample prints $1005.004832999999984 beside $1005.00. Micros fix it by making every value an integer, and integers under 253 add without error.
Is DECIMAL always the safe choice for money?
It is safer in one specific way: a decimal type defines what happens at a half, because the half exists exactly. But precision is finite, and the rounding mode has to be chosen. BigQuery rounds 1.125 half-up to 1.13 in a NUMERIC(5, 2) column, while IEEE 754 defaults to half-even. Two exact systems can still disagree by one minor unit.
Why does my order ID change after passing through JSON?
Because it travelled as a JSON number and the receiver parsed it as an IEEE 754 double. 1234567890123456789 becomes 1234567890123456768, and 9007199254740993 becomes 9007199254740992. The damage happens once and then stays stable, so nothing throws and nothing looks wrong in a diff. RFC 7493 recommends encoding integers outside [-(2**53)+1, (2**53)-1] as strings for exactly this reason.
14. Methodology & Disclosure
Every floating point value in this article was produced by execution, not by description. The same expressions were run on Node (V8, 64-bit) and on CPython 3.13; both produce identical binary64 results for every case cited, which is expected because both implement the same standard. Where the two differ in formatting rather than in value, the article quotes the JavaScript output and says so.
The derived figures are computed as follows:
- 15.9546 decimal digits is 53 × log10(2). The mantissa is 53 bits including the implicit leading one.
- The exact expansion of 0.1 and the other stored values come from Decimal conversion of the double, not from a printed representation, so they are the full value and not a truncation.
- The 1.005 rounding cases are raw
toFixed(2)return values. Nothing is rounded by the article before it is shown. - The million-row drift is a sequential loop, not a built-in
sum(), because the question is what ordered accumulation does. - The 253 and 263−1 dollar figures divide exact integers by 106 in decimal arithmetic rather than in binary, so the trailing digits are the true quotient.
- The toFixed census is a regular expression over the 194 HTML files in the published tree, matching
.toFixed(followed by a single digit, which counts calls in inline scripts. It excludes the two files inembed/.
Two figures are bounded rather than exact, and are marked as such in place. The count of zero-decimal currencies in ISO 4217 varies between editions and between summaries that include or exclude units of account, so the article cites the range and relies on the seven three-decimal and two four-decimal entries, which are stable. The exact error that produced the trailing digits in Google's documented example is not reconstructible from the documentation, because the underlying row values are not published; the article quotes the two columns as printed and attributes the mechanism to the vendor's own explanation rather than reverse-engineering it.
Vendor behaviour described here — BigQuery type precision, Stripe's minor unit requirements, Cloud Billing's export schema — is documentation, not measurement. It is accurate as published on the date below and is the kind of detail that changes without notice. Confirm it against the current documentation before you size a schema around it.
This site carries no vendor sponsorship and no affiliate links. Nothing here is paid placement, and no provider was consulted on the content.
References & Further Reading
- IETF. RFC 7493: The I-JSON Message Format, Section 2.2 — the 9007199254740991 boundary and the recommendation to encode large integers as strings. rfc-editor.org/rfc/rfc7493
- IETF. RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format, Section 6 — the statement that implementations may limit range and precision, and the interoperable integer window. rfc-editor.org/rfc/rfc8259
- IEEE. IEEE 754-2019 Standard for Floating-Point Arithmetic — binary64 layout, the 53-bit significand, and roundTiesToEven as the default rounding attribute. ieeexplore.ieee.org
- Google Cloud. Cloud Billing — Example queries for Cloud Billing data export — the total and total_exact columns, the documented sample output, and the explanation that summing floating point values directly produces rounding errors. cloud.google.com/billing/docs/how-to/bq-examples
- Google Cloud. BigQuery — Data types — NUMERIC at 38 digits of precision and 9 of scale, BIGNUMERIC at 76.76 and 38, FLOAT64 as double precision, and the half-up example for NUMERIC(5, 2). cloud.google.com/bigquery/docs/reference/standard-sql/data-types
- Stripe. Supported currencies — amounts in the currency's minor unit, the zero-decimal list, and the ISK and UGX compatibility exceptions. docs.stripe.com/currencies
- ISO 4217 currency codes, published by SIX Group on behalf of ISO — the minor unit exponent table, including the seven three-decimal and two four-decimal entries. six-group.com
- ECMAScript Language Specification, Number.prototype.toFixed and Math.round — the differing tie behaviour of the two functions. tc39.es/ecma262
- Related: GiB to GB Marketing Gap · The 730-Hour Month · UUID v4 Collision Estimator · JSON Formatter · The 191 Problem · Character Counter · Methodology
馃摐 Copyright & Attribution
© 2026 jslet Research. This article is an original work published on jslet (jslet.com). All rights reserved.
Sharing & Reprinting: You may share excerpts (up to 200 words) with a mandatory, do-follow link back to the original article URL. Full reproduction, translation, or adaptation requires prior written permission. Contact: support@jslet.com.
Model Disclaimer: The floating point values here are properties of IEEE 754 binary64 and will not change. Vendor types, precision limits, minor unit tables and export schemas are configuration and do change; the values cited reflect the documentation listed above at the time of writing. Confirm the type and precision of the column you are actually summing before you move a billing pipeline onto it.