How counting works

RoamGuard's number is an independent measurement taken on your iPhone, not a copy of your carrier's accounting. This page says exactly where it comes from, where it can drift, and what happens when you correct it.

What iOS gives an app, and what it does not

Public APIs relevant to measuring mobile data
NeedPublic APIUsed
Bytes carried per network interfacegetifaddrs(3) → if_dataYes. POSIX, no entitlement, no permission prompt.
Reboot detectionsysctl(kern.boottime)Yes. Declared in the privacy manifest under reason 35F9.1.
Cellular vs Wi-Fi, Low Data ModeNWPathMonitorYes.
Radio generation (5G / LTE / 3G)serviceCurrentRadioAccessTechnologyYes. One entry per SIM.
Which line is carrying datadataServiceIdentifierYes, to pick between lines.
Carrier name, MCC/MNC, countryCTCarrierNo. Deprecated in iOS 16; returns "--" and "65535".
Per-plan or per-app usage—No such API exists.
Roaming status—No such API exists.

The last two rows are the reason this page is long. Everything RoamGuard does is an attempt to be useful and honest in their absence.

Why not route traffic through a VPN

A local packet tunnel (NEPacketTunnelProvider) would give exact per-flow byte counts. It was rejected on purpose. It asks you to install a VPN profile and grant sweeping network access; it breaks whenever the tunnel drops; it competes with any VPN you actually want to use; and it costs measurable battery. For an app whose whole point is that nothing leaves your phone, asking to intercept every packet is the wrong trade. The kernel's interface counters give the same figure for far less.

The counters are 32-bit, and they wrap

if_data byte counters are 32-bit. They roll over every 4 GiB. On a 35 GB plan that is roughly nine roll-overs per cycle.

The naive implementation widens both readings to 64-bit, subtracts, gets a negative number, clamps it to zero, and silently loses 4 GiB every time. That single mistake is the largest source of error in data-usage apps.

RoamGuard subtracts in 32-bit modular arithmetic instead (new &- old), which recovers the true delta exactly, as long as less than 4 GiB moved between two readings.

Counter · one 4 GiB span wraps to 0
02 GiB4 GiB

Each wrap is a delta of 4 GiB minus the old reading, plus the new one — not a negative number, and not a zero.

Four other things look exactly like a wrap

If you only inspect the numbers, a reboot, a torn-down cellular context, a newly switched-on line and a silent counter reset are indistinguishable from a roll-over. Each is therefore detected by something other than the numbers.

SituationHow it is detectedWhat RoamGuard does
Counter passed 4 GiBModular subtractionRecovers the delta exactly.
Device rebootedThe kern.boottime token changedRe-anchors every context at once and counts only post-boot bytes.
A cellular context was rebuiltThe kernel interface index changed under the same nameRe-anchors that one context. Other lines keep measuring.
A line came up or went awayThe context is missing from one of the two readingsCounts that context's own bytes only; other lines are untouched.
Counters reset with no other signalThe implied throughput exceeds 1 Gbps for the elapsed timeTreated as a reset rather than 4 GiB of traffic.

The throughput guard is a last line of defence, not the main mechanism. It can only bite on short intervals: across a fifteen-minute background gap the plausible budget already exceeds 4 GiB, so any wrap is arithmetically believable. Boot time and interface identity are what actually carry the work. The guard is applied per interface, since the ceiling describes what one radio bearer can carry.

One context at a time, summed afterwards

A phone holds several cellular interfaces simultaneously — one per active data line, plus IMS/VoLTE and MMS contexts alongside them. RoamGuard resolves each of them separately and adds the results up.

Doing it the other way round breaks. Once the contexts are added together, a line coming or going invalidates the total, and re-anchoring credits everything already counted as brand new traffic. A phone sitting at 3.2 GB would book a second, phantom 3.2 GB the moment a second SIM was switched on.

Interface identity is the name and the kernel index, because iOS reuses the names. A rebuilt pdp_ip1 is a different interface with counters back at zero, and only the index says so. There is no fixed limit on how many contexts are tracked, and the same identity check applies to en0 when Wi-Fi is toggled off and on.

Because of this, an unusual event on one line costs at most that one line's traffic, for a single interval.

When a reading is taken

You open the app
A reading is taken immediately.
The network changes
NWPathMonitor reports a path change and a reading is taken.
Background refresh
A BGAppRefreshTask is requested no sooner than every 15 minutes. iOS decides whether and when to grant it.

There are no timers and no polling. That is why the battery cost is close to nothing, and it is also why a missed background window loses nothing: the kernel counters are cumulative since boot, so the next reading — in an hour or in three days — picks up everything that happened in between.

The one thing a missed window does cost is attribution. RoamGuard knows how many bytes moved, but not which network was active during the gap. Traffic is charged to cellular only when cellular is the active path at the moment of the reading, or when nothing is connected at all; the cellular interface can tick over slightly during a Wi-Fi handover, and charging that to your plan would be wrong. The “time on each network” readout in Settings is similarly a sample, not a complete log: an interval longer than 30 minutes is not attributed to either network.

Which pool a byte is charged to

Wi-Fi bytes are recorded separately and never counted against your plan.

Cellular bytes go to the home pool or the roaming pool, and the decision is made in this order:

  1. Your setting wins. Home or Roaming, set by hand in Settings, is the source of truth and is labelled on the dashboard as “You set this manually.”
  2. Region drift can suggest a change. If “Detect travel automatically” is on and your device region differs from your home region, RoamGuard infers roaming. No permission, no location access. The dashboard labels it “Estimated from your device region.”
  3. Otherwise it assumes home, and says so: “Assumed to be your home network. iOS does not report roaming status to apps.”

The screen always states which of the three produced the answer, so an estimate is never presented as a fact.

On a dual-SIM phone each line can additionally be pinned to its own pool, which the device-wide switch cannot express. That assignment is pinned to the line's kernel index: when the context is rebuilt the answer goes stale, the line falls back to the switch, and the app asks again rather than charging one line's gigabytes to the other line's pool.

The billing cycle, reboots and resets

Cycle boundaries

You pick a billing day from 1 to 31. In a month shorter than the day you picked, the cycle restarts on that month's last day — “the 31st” behaves correctly in February, and across leap years and daylight-saving changes.

Nothing is deleted when a cycle rolls over. Daily records are kept; the dashboard and the alert ledger simply scope themselves to the current cycle, so the same threshold alert fires again next month instead of staying silent forever.

Reboots

A reboot resets every kernel counter to zero at once. It is detected by the boot-time token, not inferred from the numbers, and every context is re-anchored in the same step. Bytes carried before the restart stay on the days they were recorded; bytes carried after it are counted from zero. The only traffic that cannot be recovered is what moved between the last reading and the restart itself — the kernel forgot it too.

Reset measurements

Settings › Reset measurements clears the recorded history from this device and starts counting again from the next reading. Your plan settings are kept. If iCloud sync is on, only this device's records are removed; history another device contributed is not touched.

What “match my carrier's figure” actually does

This is the honest answer to the accuracy gap, so it is worth being precise about the arithmetic.

You enter the figure your carrier reports for the current cycle. RoamGuard then:

  1. Computes what it has measured for that pool so far in the current cycle.
  2. Stores a calibration record — the reported figure, the measured figure, the date and the device — so the correction is auditable rather than invisible.
  3. Takes the difference, reported − measured, and applies it to today's record only. The result is floored at zero, so a downward correction larger than today's traffic empties today rather than going negative.
Before · measured this cycle 8.2 GB

Carrier says 9.6 GB. The difference is +1.4 GB.

After · realigned 9.6 GB

Today's record absorbs the whole +1.4 GB. Every earlier day keeps the value that was actually measured, so the history stays a record of observations rather than being rewritten.

Measurement continues normally from there. Calibration is an anchor, not a mode: nothing about how bytes are counted changes afterwards.

Why the two numbers differ in the first place

Your carrier's figure is the one that gets billed. RoamGuard exists so that figure never surprises you, not to replace it.

Where the numbers live

Daily totals
On this device, in the app's own container.
Settings and last reading
In the app group shared with the widget and the Watch app.
Widgets and Watch
Read a small published snapshot, never the database.
iCloud
Off by default. When enabled, records sync through your private iCloud database and carry a device identifier, so an iPhone and an iPad on one account are never added together.
Anywhere else
Nowhere. The app makes no network requests of its own.