Arduino millis() Rollover: Safe Timing Beyond 49 Days
Use unsigned subtraction to handle millis() rollover after 49.7 days, with a non-blocking blinker example and RTC guidance.
26 Feb 2026, 13:21 UTC

Why millis() disappears after 49.7 days
On AVR-based boards such as the Uno and Nano, millis() is driven by Timer0 and returns an unsigned long: a 32-bit unsigned integer that increments once every millisecond. After 2^32 milliseconds—about 49.7 days—the counter wraps back to zero. The micros() function wraps much sooner, roughly every 71.5 minutes.
This wrap is not a bug. It is expected behavior for a fixed-width timer. The problem appears in how your sketch compares timestamps.
The subtraction pattern that survives rollover
A common but fragile check looks like this:
if (millis() > previousMillis + interval) { ... }
If previousMillis + interval overflows the 32-bit limit, the comparison can fail or trigger at the wrong time.
Because unsigned integer arithmetic wraps modulo 2^32, subtracting two millis() readings always produces the correct elapsed time, even across the wrap:
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// do work
}
No extra rollover logic is required. The result is correct as long as both values are unsigned long and the interval is smaller than the rollover period.
Worked example: rollover-safe blinker
This sketch toggles the built-in LED every second without delay(), so the loop stays free for sensor reads or other tasks.
/*
* Rollover-safe non-blocking blinker.
* Written for Arduino Uno/Nano; other cores may use different timer hardware.
*/
const int ledPin = LED_BUILTIN;
const unsigned long blinkInterval = 1000; // milliseconds
unsigned long previousBlinkMillis = 0;
bool ledState = false;
void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(9600);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousBlinkMillis >= blinkInterval) {
previousBlinkMillis = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
Serial.print("Toggle at ms: ");
Serial.println(currentMillis);
}
}
To run it, open the Arduino IDE, choose your board and port under the Tools menu, then upload the sketch. Open the Serial Monitor to watch the timestamps. The LED should toggle roughly once per second, and the printed millisecond values should increase steadily.
If your board uses native USB, avoid leaving while (!Serial) { ; } in headless code, because the sketch can halt until a serial terminal connects.
To verify the timing behavior, upload a minimal sketch that prints millis() once per second for 20 seconds. Each line should appear about 1000 ms after the previous one, with no backward jump. This check does not prove multi-month stability, but it confirms the timer and serial path are behaving as expected.
When millis() is not enough
millis() measures time since the last reset, not wall-clock time. It returns to zero after power loss, and it is not suitable for logging absolute dates. For multi-day data logging that survives reboots, pair the board with an external real-time clock such as a DS3231 over I2C.
Also, millis() depends on the Timer0 interrupt. If your sketch disables interrupts for long periods or runs heavy code inside an ISR, timer ticks can be delayed, causing jitter. Avoid calling millis() inside time-critical ISRs expecting exact precision. Boards based on ESP32, SAMD, or other architectures implement millis() with different timers and rollover periods, so verify the core files if you port this code.
Takeaway
Use subtraction, not addition, when comparing millis() timestamps. The unsigned wrap handles the 49.7-day rollover automatically. If you need calendar time or power-loss persistence, add an RTC rather than trying to track dates with millis() alone.
To revert the example, re-upload your previous sketch or the Blink example from the IDE.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.