DEV Community

Cover image for Stop Using delay(). Your Deserves Better.
v. Splicer
v. Splicer

Posted on Originally published at Medium

Stop Using delay(). Your Deserves Better.

And so does anyone who has to use what you build.

Let's get one thing straight.

If your entire Arduino program is held together by delay(1000), you didn't write firmware. You wrote a screensaver that occasionally does something useful.

We all started there. Blink.ino tells you to do it. The Arduino gods say delay(), and we obey. Then we add a second sensor. Then a button. Then we wonder why the button only works if we press it with the patience of a monk.

This is an intervention.

The Comfortable Lie of delay()

delay() feels so intuitive. So honest. You want to wait a second? You say wait a second.
Perfect. Readable. Works. For exactly one LED and nothing else.

Here’s what delay() actually does: it grabs your poor little ATmega by the collar, slams it against the wall, and screams "DO NOTHING FOR 1000 MILLISECONDS."

It’s not sleeping. It’s not idling efficiently. It’s busy-waiting. Spinning in a tight little loop, counting clock cycles, burning power, ignoring the entire outside world.

Your button press? Ignored.
Serial data coming in? Might get dropped.
That critical sensor reading? Nope, we're busy doing nothing.

delay() is the programming equivalent of putting your fingers in your ears and going "LALALALA I CAN'T HEAR YOU."

It’s blocking. And blocking code doesn't scale.

The Blink Trap: Where Projects Go to Die

Every doomed project follows the same arc:

Phase 1: Innocence
You make an LED blink. You feel like Tony Stark.

Phase 2: Ambition
You add a second LED. You want them to blink at different rates.
Suddenly your timings are all linked. LED1 can't blink without dragging LED2 along with it. Your code looks like you tried to choreograph a dance via text message.

Phase 3: Desperation
You add a button to turn it off. You press the button. Nothing happens. You press it harder, as if force will travel through the plastic into the microcontroller.

You add delay(10) to "debounce" it. Now everything is even more laggy. You Google "why my Arduino button doesn't work" at 2 AM.

Welcome. You've hit the delay() wall. Every Arduino developer hits it. The good ones climb over it.

The Upgrade: Meet millis() - Time Without the Tantrum

The grown-up way to handle time is to not stop time.

Instead of saying "wait here," you say "remember what time it is, and check back later."

That's millis(). It returns the number of milliseconds since your Arduino woke up. It never stops. It never blocks. It just quietly counts, like a responsible adult.

Here's the mindset shift:

delay() says: "I will wait."
millis() says: "Has enough time passed yet?"

This is non-blocking timing. And it looks like this:
That subtraction currentMillis - previousMillis is not an accident. It handles the millis() overflow after ∼49 days correctly. If you use currentMillis >= previousMillis + interval, you will have a very confusing bug in 49 days. Don't be that person.

Now your loop() is free. It's spinning fast, hundreds of thousands of times per second, checking: Is it time? Is it time? Is it time? No? Cool, I'll go do other useful things.

This is what we call cooperative multitasking. You aren't using an RTOS. You're just not being rude to the rest of your code.

Pattern 1: The Two LEDs That Finally Got Divorced

This is the classic interview question. Two LEDs, independent blink rates. With delay(), impossible. With millis(), trivial.
Two timers. No drama. They don't even know each other exists. This is healthy boundaries in code form.

Pattern 2: The Button That Actually Works

Your button problem was never the button. It was you abandoning it for a full second every loop.

Here's a non-blocking button with debouncing that doesn't suck:
No delay(50) to "fix" bouncing. We timestamp the chaos and ignore it until it settles. Elegant. Responsive.

Pattern 3: You Need a State Machine (Yes, You)

Once you have more than two timings, a pile of previousMillis variables turns into spaghetti. Time to level up.

Stop thinking in linear steps. Start thinking in states.

Your project isn't a script. It's a mood ring.

Example: You want a traffic light that can be interrupted by a pedestrian button.

Linear thinking: GREEN delay RED delay GREEN... panic when button is pressed.

State machine thinking:
Each state knows what it should do, how long it should last, and where to go next. Your loop() stays fast. Your logic stays readable. You can add 20 more states without breaking everything.

This is how real embedded systems work. From your microwave to a Mars rover. Not a single delay() in sight.

But What About...?

"But delay() is easier to read!"
For a 5-line example, yes. For a 500-line project, it's unreadable because it doesn't work. Readability that hides broken behavior isn't readability. It's a lie.

"But I need precise timing!"
Then delay() is even worse. delay(1000) is not exactly 1000ms. It's 1000ms plus however long all the other code in your loop took. Your timing drifts. With millis(), your interval is fixed.

If you need really precise timing — like microsecond-level, bit-banging, driving NeoPixels — you shouldn't be using either. You should be using hardware timers, micros(), or interrupts. But that's a Medium article for when you're ready to lose your weekends.

"But what about delayMicroseconds()?"
Same sin, smaller scale. Still blocking.

The Official Cheat Sheet to Replace Every delay()

Let's make this actionable. Pin this to your wall.

Final Thought: Your Arduino Is Better Than You Think

The Arduino Uno is a 16MHz computer with 2KB of RAM. It's not powerful. But it's infinitely more capable than delay() lets it be.

When you stop using delay(), you stop treating your microcontroller like a kitchen timer and start treating it like what it is: a real-time processor that can juggle.

Your code gets faster. More responsive. More professional. You can finally add that second, third, tenth feature without everything collapsing.

So open your last project. Search for delay. Look at each one. Ask it: "Do you really need to freeze the entire universe, or are you just lazy?"

99% of the time, it's lazy.

Kill it. Replace it with millis(). Give your Arduino its life back.

Your button will thank you.


Got a horror story about delay() ruining your project? I know you do. Drop it in the comments. Let's heal together.

If this saved you a debugging night, give it a clap. Or 50. I don't use delay() to count.

Top comments (0)