Skip to content

Build numbers

Build numbers are SimpleOTA's source of truth for "is this newer than what the device is running?".

Properties

Property Behavior
Scope Per project.
Type Positive integer.
Allocation Atomic increment of Project.build_counter.
Monotonic Always strictly increasing within a project.
Reusable? Never. Even if you delete the artifact.
Gap-fillable? No. A failed upload still consumes the number.
Comparable across projects? No. Project A's 42 and project B's 42 are unrelated.

Why monotonic?

The decision engine's freshness test is purely: "Is artifact.build_number > device.current_build_number?"

That comparison is the entire definition of "newer". No semver parsing, no date games: just an integer. Devices report their current build number on every check; SimpleOTA picks the highest assigned and compatible artifact and tells the device to install it if it's higher.

How are they allocated?

Inside Project there's a build_counter integer. On artifact upload, SimpleOTA opens a transaction, takes a SELECT … FOR UPDATE on the project row, increments the counter, and assigns that value to the new artifact. This is the only safe way to guarantee monotonicity under concurrent uploads.

If the transaction commits, the number is gone even if you later delete the artifact. That's intentional; devices may already have recorded the build number, so re-using it would lie about lineage.

Device firmware integration

This is the most common source of confusion.

SimpleOTA assigns its own build number on upload. This number has nothing to do with any counter you maintain inside your firmware binary.

The correct flow

  1. First boot: device has never done OTA. Report current_build_number: 0.
  2. SimpleOTA responds with an offer: { "update_available": true, "build_number": 21, ... }.
  3. Device downloads, flashes, and reboots.
  4. Device saves 21 to NVS (or equivalent persistent storage).
  5. After reboot, device reports current_build_number: 21 on every subsequent /check/ call.

What NOT to do

Never report your own embedded version as current_build_number

If your firmware binary contains #define APP_BUILD 22 (or a git commit counter, or any internally-tracked number), do not send that as current_build_number.

SimpleOTA's decision engine compares its own artifact.build_number against device.current_build_number. If those come from different counters, the comparison is meaningless:

  • Your internal counter is 22. SimpleOTA has assigned build 21 to the same binary. The engine sees 21 ≤ 22up_to_date. The device never updates.
  • Your internal counter is 5. SimpleOTA has assigned build 21. The engine sees 21 > 5offers an update the device is already running.

The rule is simple: the value you persist in NVS after a successful OTA must be the build_number from the /check/ response, and that is the only value you ever send back as current_build_number.

Your human-readable version string (v1.4.2, git SHA, etc.) belongs in the version field — it is stored and displayed, but never used for ordering.

Common gotchas

  • Don't try to map build numbers to git tags 1:1. They're allocated on upload, not on commit. A failed CI run that gets to the upload step but errors mid-flight will still consume a number.
  • Don't expose build numbers as your user-facing version string. Use whatever is in your firmware (v1.4.2, git short SHA, etc.) for human display. Build number is plumbing.
  • Don't hardcode current_build_number in your firmware. Read it from the NVS key you wrote after the last successful OTA. On a factory-fresh device with no NVS key, send 0.