From Meter Reading to Invoice: Architecture of IoT Utility Billing

From Meter Reading to Invoice: Architecture of IoT Utility Billing
A dispute-proof IoT utility billing system always has the same shape: five stages, from raw meter reading to issued invoice. I've built this pipeline for building utilities — electricity and water submetering across multi-tenant sites — and the shape stays consistent every time.
Most IoT dashboards only need to be plausible: if a chart is off by a reading or two, nobody notices. Billing is a different story. The number at the end of the pipeline is a claim on someone's money, and sooner or later a tenant will dispute it. Every architectural decision in a metering-to-billing system should be made with that dispute in mind.
Stage one: ingestion
Utility meters mostly speak two dialects. Pulse outputs close a contact every N liters or watt-hours, and something has to count the pulses. Picture someone standing at a door clicking a tally counter for every guest — look away for a moment and that count is gone for good. Register reads expose a cumulative total over Modbus RTU on RS-485, closer to a car odometer. A gateway polls the meter as Modbus master every few seconds and publishes the values over MQTT to the backend.
Prefer cumulative registers wherever the meter offers them. A pulse counter that goes offline loses consumption forever — the pulses happened, nobody counted them. A cumulative register is self-healing: when the gateway comes back, the next read includes everything that happened during the outage. This one choice at the bottom of the stack decides how painful gap handling is at the top.
Ingestion itself stays thin: validate the reading, stamp it with meter identity and time, store it raw in a time-series table. No math here — raw readings are the evidence everything downstream gets audited against.
Stage two: interval aggregation
Raw readings arrive every few seconds; tariffs and invoices think in hours, days, and months. The aggregation stage rolls raw cumulative readings into consumption intervals: consumption for an interval is just the delta between the register value at its end and at its start.
Keep intervals in their own table, derived from the raw data, never replacing it. When a tenant questions a bill, you walk the chain backwards: invoice line, to the intervals it covers, to the raw readings inside each interval. Aggregates that overwrite their inputs can't answer that question.
Stage three: the tariff engine
The tariff engine converts consumption into money: price per kWh or per cubic meter, consumption tiers, fixed monthly charges, per-tenant or per-building overrides. Two rules keep it defensible:
- Tariffs are versioned, never edited. A price change is a new tariff row with a validity window. The tariff that priced an old invoice must still exist, unchanged, when the dispute arrives a year later.
- The invoice records which tariff version priced it. Recomputing an old invoice must reproduce the old result exactly — not the result under today's prices.
Stage four: billing cycles and invoice generation
Different tenants bill on different rhythms: daily for prepaid-style arrangements, weekly for short-term commercial tenants, monthly for everyone else. A scheduler walks the billing plans and generates invoices when each cycle closes.
Cycle boundaries are calendar concepts, so the scheduler must be timezone-aware: "the end of June 30th" is a different instant for every site, and a portfolio spanning regions has to group readings by each site's local day — a problem I covered in grouping time-series by day across timezones in PostgreSQL.
Generation is two steps, not one: the system produces a draft invoice an operator can review, then a human or an approval rule issues it. Once issued, the invoice is immutable — corrections happen as credit notes or adjustment lines, never edits.
Missed readings and gaps
Gateways lose power. Networks drop. The system's honesty during gaps is what separates a billing platform from a spreadsheet.
With cumulative registers, a gap costs you resolution, not money: the first reading after the outage covers the whole silent period, and you allocate it across the gap as estimated intervals, flagged as such. With pulse inputs the consumption is simply gone, and the only defensible options are estimation from historical profile — again flagged — or an explicit meter-read visit.
Either way, estimated quantities appear on the invoice as estimated. Hiding the flag is how a minor outage becomes a billing scandal.
Quarantine helps too: readings that jump implausibly (a register that goes backwards, a delta beyond the meter's physical capacity) get held out of the billing path until someone releases them. Meter swaps are the classic cause — a new meter starts from zero, and without a handover record the delta looks like a tenant filled a swimming pool overnight.
The audit trail
Everything above converges here. For every issued invoice, the system can produce: the raw readings, the intervals derived from them, the tariff version applied, who generated the draft, who issued it, and every estimate flagged along the way.
That chain is the product. Tenants don't trust a bill because the dashboard is pretty. They trust it because when they ask "where does this number come from," the answer arrives in seconds and holds up.
The takeaway
Ingest raw and keep it forever. Aggregate into intervals without destroying the source. Price through versioned tariffs. Generate drafts that humans issue. Flag every estimate.
Do that and disputes become lookups instead of arguments. The broader plumbing underneath — brokers, ingestion, live dashboards — is covered in the complete guide to IoT backends.
Related Posts
Building something similar?
IoT Backend & Multi-Protocol Integration
Backends that ingest device telemetry across MQTT, WebSocket, Modbus, and BLE, and normalize it into reliable real-time dashboards.
See how I can help