Skip to content
BoilerBuddy

API

Buffering and the clock

Keeping readings locally and sending them in order, catching up after an outage within the limits, and having readings from before clock sync corrected.

This is how your device keeps measuring while the internet is down, catches up afterwards without losing readings, and gets readings taken before the clock was set stored at the right time.

Store first, then send

Write every reading to a local buffer first, for example SQLite, and send from there: oldest first, in batches of at most 1000. That way measuring never waits for the network.

Answer What to do
200 Remove the batch from the buffer, except the rejected readings with retry: true. Those go in a later batch
No answer, a timeout, 429 or 5xx Keep the batch and send it again later, with the same batch_uuid. Whatever the server already had counts as duplicates
422 The batch is not valid as a whole and sending it again won't help. Set it aside, so it does not block the rest, and look for the bug
401 Keep everything. With a new token you can still send it all
  • A 200 that does not come from the API, such as a hotspot login page, confirms nothing. Check that the answer is JSON with accepted.
  • Buffer full? Drop the oldest readings, not the newest. For comparison: the BoilerBuddy Alpo keeps at most a million, a little over two months at nine sensors per minute.

Catching up after an outage

Per token you may upload 10 batches of 1000 readings per minute. A week without internet with 8 sensors measuring every minute is 8 × 1440 × 7 = 80,640 readings: about nine minutes to catch up at full speed.

  • Leave room for config and status within your 60 requests per minute. At most 8 batches of 500 per minute is a safe pace.
  • Readings older than 365 days get measured_at_too_old.
  • A reading that falls on an eighth calendar day (UTC) in the batch gets retry: true and goes in the next one. If you send in order, you won't reach that.
  • More than 5 minutes in the future is rejected. A clock that runs fast costs you readings: keep it in sync with NTP.

The clock

DIY hardware such as a Raspberry Pi or ESP32 usually has no battery-backed clock. After power-on the time is wrong (1970, or the moment it was switched off) until NTP sets it. You can:

  1. Wait to measure until the clock is set. The least work: you only miss the first minute or so. The ESP32 example does this.
  2. Measure anyway and let the server correct, with clock_synced: false and the monotonic clock. See below.
  3. Use the Date header from any server answer as the time source for the clock anchor while NTP has not synchronised yet.

How the correction works

For every reading taken before the clock is set, include:

  • clock_synced: false. A real false: "false" or 0 gives invalid_item.
  • boot_id: an id for this boot. On Linux it is in /proc/sys/kernel/random/boot_id; on a microcontroller, generate a random one at every start.
  • mono_ns: the monotonic clock in nanoseconds at the moment of measuring. It counts from startup and never jumps.
  • measured_at is still required and must be a valid timestamp. Send whatever your clock says.

The batch then carries a clock anchor: the clock at the moment of sending.

"clock": {"boot_id": "4b8e7c1d-…", "synced": true, "sent_at": "2026-10-15T09:31:02Z", "sent_mono_ns": 72000000000}

The server computes measured_at = sent_at − (sent_mono_ns − mono_ns). That only works if the anchor has synced: true, the reading's boot_id equals the anchor's, and mono_ns is an integer no greater than sent_mono_ns. After that the usual limits apply: at most 5 minutes ahead, no older than 365 days.

If it does not work, the reading gets clock_unsynced:

  • No anchor, or an anchor with synced: false: retry: true. Send the reading again once your clock is set. As long as the device has not restarted, the server can still place it.
  • A synchronised anchor, but a different boot_id or no usable mono_ns: retry: false. The reading is from an earlier boot and can never be placed. So send unsynchronised readings soon after the clock is set, before a restart.

An example

A device starts up with its clock at 1970. After 12 seconds it measures T1: 48.2 °C, with clock 1970-01-01T00:00:12Z and monotonic clock 12,000,000,000 ns. After 40 seconds NTP sets the clock, and after 72 seconds the batch goes out:

{
  "clock": {"boot_id": "4b8e7c1d-2f3a-4c5b-8d6e-9f0a1b2c3d4e", "synced": true,
            "sent_at": "2026-10-15T09:31:02Z", "sent_mono_ns": 72000000000},
  "readings": [
    {"sensor": "T1", "value": 48.2, "measured_at": "1970-01-01T00:00:12Z",
     "clock_synced": false, "boot_id": "4b8e7c1d-2f3a-4c5b-8d6e-9f0a1b2c3d4e",
     "mono_ns": 12000000000}
  ]
}

The difference is 72 − 12 = 60 seconds, so the reading is stored at 09:31:02 − 60 s = 2026-10-15T09:30:02Z.

Had the batch gone out after 30 seconds, with an anchor with synced: false, the answer would have been {"index": 0, "reason": "clock_unsynced", "retry": true}. The device then keeps the reading, and after 72 seconds it goes through.

Stuck?

Ask your question through the contact form. We'll help you out and make this page better.

Contact us