Implementing Non-blocking Timing in Arduino with millis()
Learn how to replace the blocking delay() function with millis() in Arduino to enable multitasking and responsive sensor polling.
02 Jan 2026, 09:32 UTC

The Problem with delay()
The delay() function pauses the entire processor. While the board is waiting for a timer to expire, it cannot read sensor data, poll buttons, or update displays. This "blocking" behavior makes it impossible to handle multiple tasks simultaneously—for example, blinking an LED every 500ms while simultaneously monitoring a temperature sensor for a critical threshold.
The solution is to use millis(), a function that returns the number of milliseconds elapsed since the board started. By comparing the current time to a stored timestamp, you can create a "heartbeat" for specific tasks without stopping the rest of the program.
Prerequisites
- An Arduino-compatible board (e.g., Uno, Nano, ESP32).
- Arduino IDE installed.
- Basic understanding of
unsigned longdata types.
Implementing the Non-blocking Timer
To replace a blocking delay, you must track the last time a specific action occurred using a global variable. This allows the loop() function to run thousands of times per second, checking if the required interval has passed before executing a task.
Step 1: Define Timing Variables
You must use unsigned long for time variables. A standard int or long will overflow too quickly, causing the timer to fail after a few seconds or minutes.
const long ledInterval = 500; // Interval in milliseconds
unsigned long previousMillis = 0; // Stores the last time the LED updated
Step 2: Create the Comparison Logic
Inside the loop(), capture the current time and subtract the previous timestamp. If the difference is greater than or equal to your interval, trigger the event.
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= ledInterval) {
// Save the last time you blinked the LED
previousMillis = currentMillis;
// Perform the action
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}
// Other code here runs immediately without waiting for the LED
}
Practical Example: Dual-Rate Multitasking
The following configuration demonstrates two independent timers running on a single board. This setup allows a fast-blink LED and a slow-blink LED to operate without interfering with each other.
const int ledFast = 12;
const int ledSlow = 13;
unsigned long prevFast = 0;
unsigned long prevSlow = 0;
const long intervalFast = 200;
const long intervalSlow = 1000;
void setup() {
pinMode(ledFast, OUTPUT);
pinMode(ledSlow, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - prevFast >= intervalFast) {
prevFast = now;
digitalWrite(ledFast, !digitalRead(ledFast));
}
if (now - prevSlow >= intervalSlow) {
prevSlow = now;
digitalWrite(ledSlow, !digitalRead(ledSlow));
}
}
Critical Engineering Considerations
Handling Timer Overflow
The millis() counter resets to zero after approximately 49.7 days. A common mistake is to use addition (e.g., if (currentMillis >= previousMillis + interval)). This will fail when the counter overflows.
By using subtraction (currentMillis - previousMillis), the unsigned math handles the wrap-around automatically, ensuring the timer remains accurate indefinitely.
Precision and Drift
Updating the timestamp with previousMillis = currentMillis; is the most common method, but it can introduce slight drift if the loop is heavily loaded. For high-precision timing, use previousMillis += interval; to maintain a consistent cadence, though this can lead to "catch-up" bursts if the processor falls significantly behind.
Verification and Testing
To verify the non-blocking implementation is working correctly:
- Upload the Dual-Rate example. Confirm both LEDs blink at their respective frequencies.
- Add a
digitalRead()for a push-button in theloop()that triggers a Serial print. - Press the button while the LEDs are blinking. If the Serial message appears instantly, the timing is non-blocking. If there is a perceptible lag, a
delay()call is likely still present in the code.
Rollback
Since this is a logic change rather than a hardware configuration, rollback consists of reverting the loop() structure to a linear sequence using delay(). Note that this will disable all concurrent tasking.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.