Back to blogEmbedded Systems

Non-Blocking Arduino Code: Replacing delay() with millis()

5 min read·Aug 2026

Writing non-blocking Arduino code by replacing delay() with millis() is the first step toward writing reliable multi-tasking embedded firmware. While delay() halts the CPU completely, millis() lets the processor poll buttons, read serial data, and drive motors continuously.

1. The fundamental flaw of blocking delays

When an Arduino executes delay(2000), all peripheral polling stops for two seconds. During this window, physical button clicks go undetected, incoming serial packets overflow hardware buffers, and PID control loops lose real-time stability.

2. The timing template structure

Asynchronous timing relies on checking the elapsed runtime counter. Use this standard boilerplate:

  • Declare unsigned long previousMillis = 0; outside the loop.
  • Set an interval constant: const unsigned long interval = 500;.
  • Evaluate the difference inside loop(): if (millis() - previousMillis >= interval).
  • Update the tracker: previousMillis = millis(); inside the condition block.

3. Managing rollover safety after 50 days

The millis() counter overflows back to zero after approximately 49.7 days. Because subtraction on unsigned integers wraps around cleanly in C++, writing millis() - previousMillis >= interval remains mathematically sound even across an overflow event.

4. Combining millis() with Finite State Machines (FSM)

Replace nested delays in sequence logic with an enum State structure evaluated via switch/case statements. For larger platforms, compare these techniques against our guide on Arduino vs Raspberry Pi[cite: 2].

When to get help

If you'd rather have this handled directly, see our Embedded Firmware Architecture Services for scope and turnaround times.