From Blocking to Cooperative: Replacing delay() with millis() on Arduino UNO
Learn how to replace blocking delay() calls with non‑blocking millis() timing on Arduino UNO, enabling concurrent tasks like LED blinking and serial printing. Includes a step‑by‑step example, overflow‑safe subtraction, trade‑offs, and verification steps.
23 Apr 2026, 11:04 UTC

Problem: The Blocking Nature of delay()
On Arduino UNO‑class boards, delay(ms) stops the entire sketch for the specified time. During that pause, the processor cannot read buttons, respond to serial data, or sample sensors. If you need several tasks to run concurrently—say, blinking LEDs, reading a sensor, and printing status—you end up with nested delay() calls or a single long delay() that stutters the whole system.
Why millis() is the Right Tool
millis() returns the number of milliseconds that have elapsed since the board powered on, stored in an unsigned long (32‑bit). Because it never blocks, you can poll it each loop iteration and decide whether a task should run. The key pattern is:
unsigned long previous = 0;
const unsigned long interval = 500; // 0.5 second
void loop() {
unsigned long now = millis();
if (now - previous >= interval) {
previous = now;
// perform the periodic action
}
}
The subtraction now - previous is safe even when millis() rolls over after ~49.7 days, because unsigned arithmetic wraps correctly. Avoid the less‑safe comparison now >= previous + interval, which can fail on rollover.
Concrete Example: Two LEDs + Serial Status
Below is a single loop() that drives two LEDs at different rates and prints a status message every two seconds, all without delay(). Replace the LED pins and serial baud rate with your own values.
#include <Arduino.h>
const uint8_t ledPin1 = 13; // built‑in LED on most boards
const uint8_t ledPin2 = 12; // external LED
const unsigned long blinkInterval1 = 250; // 250 ms
const unsigned long blinkInterval2 = 1000; // 1 s
const unsigned long serialInterval = 2000; // 2 s
unsigned long prevBlink1 = 0;
unsigned long prevBlink2 = 0;
unsigned long prevSerial = 0;
bool state1 = false;
bool state2 = false;
void setup() {
pinMode(ledPin1, OUTPUT);
pinMode(ledPin2, OUTPUT);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
// Blink LED 1
if (now - prevBlink1 >= blinkInterval1) {
prevBlink1 = now;
state1 = !state1;
digitalWrite(ledPin1, state1);
}
// Blink LED 2
if (now - prevBlink2 >= blinkInterval2) {
prevBlink2 = now;
state2 = !state2;
digitalWrite(ledPin2, state2);
}
// Serial status
if (now - prevSerial >= serialInterval) {
prevSerial = now;
Serial.print("LED1: ");
Serial.print(state1 ? "ON" : "OFF");
Serial.print(", LED2: ");
Serial.println(state2 ? "ON" : "OFF");
}
}
When you upload this sketch, you should see the two LEDs toggle independently—LED 1 every 250 ms, LED 2 every 1 s—while the serial monitor prints the current state every 2 s. No blocking occurs; the loop() runs continuously, checking each timestamp in turn.
Trade‑Offs and Limitations
- Cooperative, not preemptive: If a single iteration of
loop()takes longer than an interval (e.g., a slow blocking sensor read), that task will run late and may cascade delays to others. - Accuracy:
millis()increments every 1 ms, but the actual timing of your action depends on how longloop()takes. For sub‑millisecond or jitter‑critical timing, use hardware timers or interrupts. - Interrupts: While interrupts are active,
millis()does not advance. If you rely onmillis()inside an ISR, you may get incorrect results. - Rollover: After ~49.7 days,
millis()resets to zero. The subtraction idiom keeps the code working across this rollover, but any logic that depends on absolute time (e.g., a real‑time clock) must handle wrap‑around explicitly.
How to Verify Your Implementation
- Run the example: Open the serial monitor and watch the LEDs and printed status. Both LEDs should maintain their own intervals regardless of each other.
- Test rollover safety: In the sketch, add a line like
prevBlink1 = 0xFFFFFFF0; // 16 ms before rolloverbeforeloop()starts. Upload and confirm that the 250 ms interval still triggers correctly after the overflow. - Measure loop latency: Add
digitalWrite(LED_BUILTIN, HIGH);at the start ofloop()anddigitalWrite(LED_BUILTIN, LOW);at the end. Use an oscilloscope or logic analyzer to confirm the pin toggles once per iteration, indicating no blocking.
Actionable Takeaway
Replace every delay() in a sketch that needs concurrent behavior with the millis() subtraction pattern. Keep each task’s code short, avoid blocking calls, and test that the loop still runs fast enough. When you need real‑time precision or preemptive multitasking, consider using timers or an RTOS. For most hobbyist and prototyping projects, millis() gives you a simple, reliable way to run multiple periodic tasks on the Arduino UNO.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.