LATEST: 7 posts daily — circuits, robotics, history, tech news, Arduino, DIY builds & more
Stop Using delay() in Arduino — Use millis() Like a Pro
Arduino Firmware Embedded Systems Electronics IoT

Stop Using delay() in Arduino — Use millis() Like a Pro

4 min read

After reading this guide, you'll be able to write Arduino code that handles multiple tasks simultaneously — blinking LEDs, reading sensors, and responding to buttons — all without freezing up. No more choosing between a responsive button and a blinking light. You'll ditch delay() forever and replace it with the professional-grade non-blocking timing pattern that every serious maker uses.

Before You Start — Prerequisites, Tools Needed, Safety Notes

You don't need much. But you do need the right mindset: stop thinking about your code as a recipe that runs top-to-bottom. You're about to think in events and timestamps instead.

  • Any Arduino board — Uno, Nano, Mega all work identically for this
  • Arduino IDE (version 1.8.x or 2.x)
  • Basic familiarity with void setup() and void loop()
  • At least one LED with a 220Ω resistor to test your output visually
  • USB cable for uploading sketches

Safety note: No high voltages here. This is pure firmware work. Still, always double-check your resistor values before powering LEDs — a dead LED is a frustrating debugging session you don't need.

Understanding The Basics — Why delay() Is Killing Your Code

delay(1000) tells your Arduino to do absolutely nothing for one full second. It's not waiting politely — it's catatonic. During that second, it can't read a button, update a display, or respond to a sensor. Your entire program is frozen.

millis() is the alternative. It returns the number of milliseconds since the Arduino powered on — a continuously running clock. Instead of pausing the program, you check the clock and ask: "Has enough time passed?" If yes, do the thing. If not, move on and check again next loop cycle.

This is called non-blocking code. Your loop() runs thousands of times per second, and each task only acts when its time is due. Everything else keeps running in parallel. It's the difference between a waiter who stands at your table staring until your food is ready versus one who serves ten tables at once.

The core formula is simple: currentTime - previousTime >= interval. When that condition is true, fire your action and reset previousTime to currentTime.

Electronics circuit detail
A closer look at the circuit in action

Step-By-Step Guide — Converting delay() to millis()

  1. Identify every delay() in your sketch. Search your code and flag each one. Each delay() is a task that needs its own timer variable.
  2. Declare timer variables at the top of your sketch — outside all functions. For each timed task, create an unsigned long: unsigned long previousMillisLED = 0;. Use unsigned long, not intmillis() returns a large number that overflows a regular integer in 32 seconds.
  3. Set your interval as a constant: const long intervalLED = 500; — this is your equivalent of the number inside delay().
  4. In void loop(), capture the current time at the very top of the loop: unsigned long currentMillis = millis(); Do this once per loop cycle and reuse it everywhere.
  5. Verify by adding a second independent task — a button read or serial print with its own timer. Both should operate without interfering. If they do, your non-blocking conversion is working correctly.

Replace the delay block with a conditional check:

if (currentMillis - previousMillisLED >= intervalLED) {
  previousMillisLED = currentMillis;
  // Your action here — toggle LED, read sensor, etc.
}

Expected result: Your LED blinks at the same rate as before — but your loop continues running freely between blinks.

Pro Tips — What Separates Beginners From Experienced Builders

  • Always use unsigned long for time variables. The subtraction currentMillis - previousMillis handles the millis() rollover (which happens after ~49 days) correctly only with unsigned math. Don't get clever with int — it will bite you.
  • Never put delay() inside an interrupt service routine. It doesn't work and will hang your board. If you're using interrupts, millis() is your only friend.
  • Create a task struct or class for complex projects. When you have five timers, abstract them. A simple struct holding previousMillis, interval, and a function pointer keeps your code clean and scalable.
  • Use micros() for precision timing under 1ms. millis() has ~1ms resolution. For servo control or high-speed PWM logic, switch to micros() — same pattern, higher precision.
  • Keep your loop body fast. Non-blocking code only works if nothing else inside loop() is slow. Avoid Serial.println() on every cycle — buffer your outputs or throttle them with their own timer.
Electronics engineering
Engineering precision — every component counts

Frequently Asked Questions

Q: Can I ever use delay() anymore?
Yes — in setup, for one-time initialization sequences where blocking is acceptable. Once your main loop is running, cut it out entirely.

Q: What happens when millis() overflows after 49 days?
Nothing bad, as long as you use unsigned long variables and subtraction (not comparison with absolute values). The math wraps correctly. This is a non-issue in practice.

Q: My timing seems slightly off. What's wrong?
Your loop body probably has a slow operation — a blocking Serial call, a slow library function, or leftover delay() somewhere. Profile each section and eliminate the bottleneck.

Q: Does this work with libraries that use delay() internally?
Some libraries are poorly written and block internally. Check the library source. If it uses delay(), look for an async alternative or rewrite the relevant portion. This is a real issue with some LCD and sensor libraries.

Watch the full tutorial on CircuitMasters YouTube →

▶ Watch more on CircuitMasters YouTube