Eighteen kilograms of refrigerant leaked from a chiller. Our system reported it as eighteen kilograms of CO₂ equivalent. The correct figure was 37,584.

Key Takeaways
  • A wrong emission factor produces a plausible number, so nothing downstream can catch it.
  • The real bug was not bad matching. It was choosing silently between candidates that disagreed enormously.
  • The resolver now downgrades data quality and names the spread whenever matching factors disagree materially.
  • Global warming potentials change between IPCC reports, so they are data to govern, not constants.

Nothing crashed. No validation failed. No warning appeared anywhere in the interface. The inventory showed a total, the charts drew, the report generated, and every part of it looked exactly as it looks when everything is right.

This is the failure mode that matters in carbon accounting, and it is worth describing precisely.

What happened

Refrigerants are the most potent greenhouse gases most companies handle. R-410A, the gas in a great many commercial chillers, has a global warming potential of 2,088. One kilogram of it does the atmospheric work of just over two tonnes of carbon dioxide.

Our emission factor library, imported from published sources, contained 142 refrigerant factors. Carbon dioxide at 1. Methane. R-32, R-134a, R-410A at 2,088, sulphur hexafluoride at 22,800, and a long tail of blends.

All 142 shared one activity type: refrigerant.

So when a record said "18 kg, refrigerant", the resolver had 142 candidates and no way to choose between them. It took the first row. The first row was carbon dioxide, with a factor of 1.

Eighteen times one is eighteen. The figure was reported as a clean match, with no fallback flag, no quality downgrade, and no note in the calculation trace suggesting anything unusual had occurred.

Why this class of bug is the dangerous one

A crash is a good outcome. Somebody notices immediately and it gets fixed before anything is published.

A wrong emission factor produces a number in the right units, of a plausible magnitude, formatted like every other number on the page. It flows into the scope totals, the year-on-year comparison, the intensity ratios, the disclosure questionnaire and the signed report. Nothing along that path has any way to tell it is wrong, because from every downstream perspective it is simply a number.

And it understates. A missing record is a gap someone eventually asks about. A silently substituted factor makes the footprint look better, which is the direction nobody investigates.

If this had reached a real inventory, the plant's entire fugitive emissions line, the second largest component of its Scope 1, would have read as approximately zero. And "approximately zero refrigerant leakage at a tropical manufacturing site with chillers" is a claim that an assurance provider would eventually have questioned, in the worst possible setting.

The fix was not a better lookup table

The obvious response is to improve the matching. Map refrigerant records to specific gases, add a lookup, tighten the codes.

That would fix this case and leave the class of bug intact. The real problem was not that the resolver chose badly. It was that it chose at all, silently, from a set of candidates that disagreed by three orders of magnitude.

So the resolver now checks whether its candidates agree. If several factors match an activity and their values fall within about ten percent of each other, which is what genuinely happens when two publishers report the same thing, it proceeds as before. If they disagree by more than that, it stops treating the choice as routine:

  • the data quality is downgraded to proxy, the lowest tier we have
  • the calculation trace carries a note naming the spread: 142 factors match [refrigerant] and they disagree: from 1 to 22,800. Used "Carbon dioxide" because it sorted first, which is not a reason. Choose the factor explicitly on this record.
  • the warning propagates into the analysis screens and into the report's uncertainty section

The record still calculates. We did not want a hard failure, because a blocked record tends to get worked around. We wanted the figure to arrive carrying an explicit statement that nobody had actually decided which gas this was.

The phrase "which is not a reason" is in the production code. It is there because the person reading that message six months from now needs to understand that row order is not a methodology.

The same problem, one level up

Global warming potentials have a related property that surprises people. The multipliers are not constants. They are revised with each IPCC assessment report.

MethaneAR4: 25. AR5: 28. AR6: 29.8.
Nitrous oxideAR4: 298. AR5: 265. AR6: 273.
Sulphur hexafluorideAR4: 22,800. AR5: 23,500. AR6: 24,300.

Same gas. Same leak. Three different answers depending on which report you follow, and note that nitrous oxide went down between AR4 and AR5 before rising again. The revisions are not a steady march towards larger numbers, so you cannot even reason about the direction of the difference without checking.

Switching a methane-heavy inventory from AR4 to AR6 moves that line by nineteen percent, with no change in operations whatsoever. Which set you use is therefore a disclosure decision, not a technical detail. Different frameworks mandate different ones, and comparability across years depends on stating which was applied.

Most carbon accounting tools treat GWPs as constants hard-coded somewhere in the calculation. We treat them as data that needs governing: 148 values, each with its source, its assessment report, and a verification state.

Until a value has been confirmed against its published table by a person, every figure derived from it carries a caveat, and that caveat follows the number all the way into the report. Confirming requires naming what it was checked against. You cannot simply tick it.

What we would say to anyone building this

Three things generalise beyond our particular bug.

A wrong factor does not look wrong. Any design that relies on someone noticing an implausible output will fail, because the output is not implausible. It is a number. The system has to catch the ambiguity at the point of selection, not at the point of review.

Silence is a decision. Choosing the first of 142 candidates is a choice the software made on the organisation's behalf and did not mention. Anywhere your system picks from alternatives, ask what it does when the alternatives disagree, and whether it says so.

Understatement is harder to find than overstatement. Nobody audits a number that makes the picture look better. That asymmetry means the checks have to be built in, because the incentive to go looking is not there.

We found ours while assembling a demonstration dataset. The refrigerant line was suspiciously small, and it was suspicious only because we happened to know what R-410A should produce. That is not a control. It is luck, and it is precisely why the guard now exists in the code instead of in somebody's head.

We build carbon accounting and ESG reporting software for Southeast Asian companies. If your inventory has a refrigerant line, it is worth checking what factor produced it.