millis() returns an unsigned long that wraps back to zero after about 50 days (micros() wraps after about 71.6 minutes). Devices that run for months hit this, and the bug shows up only then.
The rule
Only ever compare durations (a difference of two timestamps), never two timestamps.
unsigned long start = millis(); // always unsigned long
// Correct: keeps working across the rollover
if (millis() - start >= interval) { /* ... */ }
// Broken: fails when start + interval wraps past zero
if (millis() >= start + interval) { /* ... */ }
Why it works: unsigned subtraction is modular. If start is 5 ms before the wrap and millis() is 10 ms after it, millis() - start is still 15, the real elapsed time.
Checklist
- Store timestamps in
unsigned long(oruint32_t), neverint,longorfloat. - Don't try to detect the rollover and correct for it; write rollover-safe comparisons instead.
- For periodic tasks, advance the reference by the interval (
previous += interval;) to avoid drift, and keep the comparison in the subtraction form. - A single duration must stay below the wrap period (about 49.7 days for
millis()); for longer spans, count days separately or use an RTC. - The same applies to
micros(), with a much shorter wrap period.
Test it by temporarily starting from a timestamp close to the wrap, instead of waiting 50 days.
Sources: Stack Exchange (CC BY-SA 4.0) — see links.