Non-Blocking Timing in Arduino Using millis()
Learn how Arduino's millis() enables non-blocking delays, with a working code example, overflow handling, and common pitfalls to avoid.
05 Apr 2026, 01:50 UTC

The Mechanism
The millis() function returns an unsigned long representing the number of milliseconds since the board was reset. Because the value wraps around after approximately 50 days (2^32 ms), the standard pattern for measuring intervals subtracts the earlier reading from the later one, letting the unsigned arithmetic handle wraparound naturally.
// Non-blocking LED toggle every 500 ms
const unsigned long interval = 500;
unsigned long lastToggle = 0;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(9600);
}
void loop() {
if (millis() - lastToggle >= interval) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
lastToggle = millis();
}
// Other non-blocking work can go here
Limits and Overflow
An unsigned long ranges from 0 to 4294967295. At a 1 ms tick rate, this corresponds to about 50 days before the counter resets to zero. If your sketch runs continuously beyond that window, the subtraction millis() - lastToggle still yields the correct elapsed interval because the underflow wraps around in unsigned arithmetic. However, code that compares millis() less than lastToggle or stores the result in a signed type may produce negative values and incorrect behavior.
To verify the pattern, upload the sketch to an Arduino Uno (or compatible board) using the Arduino IDE. After upload, open the IDE's Serial Monitor at 9600 baud; you should see no flood of messages unless you add debug prints, and the built-in LED will toggle once every half-second. The sketch continues to loop, so any other code in loop() runs without waiting for the 500 ms interval to elapse. If you wish to test overflow behavior, you can simulate a long runtime by resetting the board after a deliberate delay or by using a watchdog timer, but in practice the unsigned subtraction handles wraparound correctly as long as both readings come from the same millis() call pattern.
Common Mistakes
- Placing delay() after establishing a millis() pattern. Even a single delay(100) in loop() re-introduces blocking behavior and defeats the non-blocking intent.
- Using millis() inside an ISR (interrupt service routine). The function is not atomic and may return inconsistent values when called from context that disables interrupts.
- Checking millis() less than lastToggle instead of millis() - lastToggle >= interval. This comparison fails when the counter wraps.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.