Source: https://mokxi.com/learn/arduino-blink-without-delay
Updated: 2026-09-27

Learn

# Why delay() blocks, and what to use instead

Written by the Mokxi team, updated September 27, 2026

Sign up free or see every lesson

- 192 parts on the bench
- 25 boards running now
- 1.00x real time, on every board

Click to open it in the editor

Pin 13 blinks every 250 ms, pin 12 every 400 ms, from one loop that never calls delay().

`delay(500)` does exactly what it says: it stops the sketch, entirely, for five hundred milliseconds. Nothing else in `loop()` runs, no other pin changes, no serial data is read, until the wait is over. That is fine for a single blinking LED, because there is nothing else the sketch needs to be doing in the meantime. It stops working the moment there are two things that need to happen on their own schedule, because a second `delay()` can only run after the first one finishes, which means the two things can never actually be independent.

This is worth building once specifically because most beginner tutorials teach `delay()` first, for good reason: it is the easiest possible way to make an LED blink, and it is correct for that one job. The trouble only shows up once a sketch grows past its first LED, which is exactly the moment a beginner is least prepared for a change in how timing works, so it is worth meeting the limit deliberately here, on a small circuit, rather than meeting it for the first time in the middle of a larger project.

The circuit above is the demonstration: two LEDs, one on pin 13 blinking every 250 milliseconds and one on pin 12 blinking every 400 milliseconds, both timed from the same loop with no `delay()` anywhere in it. Watch them for a few seconds and the two rates visibly drift in and out of phase with each other, because 250 and 400 do not share a common multiple that keeps them lined up, which is exactly what two independent rates should do, and exactly what two lamps sharing one `delay()` call could never manage.

Download the lesson plan (PDF)

## What delay() actually does

On the real chip, and in Mokxi's model of it, `delay()` is not a busy loop spinning the CPU for the sake of it: the Uno's own `blink` sketch relies on a hardware timer tick and lets the core sleep between ticks, which is efficient and correct for exactly one job at a time. The problem was never that `delay()` is wasteful. The problem is that it is total: for as long as it runs, the sketch can do nothing else at all, including checking a second timer that is supposed to be running independently.

## The state-machine shape

The fix is to stop waiting and start asking the time. For each thing that needs its own schedule, keep the moment it last happened, and on every pass through `loop()` ask whether enough time has gone by: `if (now - lastTime >= INTERVAL) { lastTime = now; ...act... }`. `loop()` itself never stops; it just does nothing, most passes, for whichever timers have not come due yet.

Two details in that line are not optional. `now` and `lastTime` have to be `unsigned long`, because that is the type `millis()` returns and the type the subtraction has to happen in. And the comparison has to be a subtraction, `now - lastTime`, rather than an addition like `lastTime + INTERVAL`, because subtracting two unsigned longs comes out correct even across the roll-over that happens roughly every 49.7 days, when `millis()` wraps back to zero. An addition-based comparison breaks exactly at that boundary; the subtraction never does.

This shape scales past two timers with no change to it at all. The Uno's built-in `cpptiming` example runs three lamps at three unrelated rates, 200, 330 and 1100 milliseconds, from a small array of little structs, each holding its own pin, interval and last-fired time, and one loop that checks all three the same way this page's two-lamp version checks two. It also prints the longest a single pass through `loop()` has ever taken, which stays at 0 or 1 millisecond as long as nothing in the sketch blocks, and climbs the moment something does. That number is a genuinely useful thing to watch in a sketch you are not sure is blocking anywhere: a `loop()` that is meant to be instant and suddenly is not has a `delay()`, or something that behaves like one, hiding in it somewhere.

## Two timers at once

The Uno's built-in `tworates` program is the circuit above, running as shown: two independent jobs, two intervals, one loop, no `delay()`.

firmware/examples/tworates (the exact program running above)

## What you would try with delay(), and why it stops scaling

It is possible to make two lamps alternate with nothing but `delay()`, and the Uno's built-in `twoleds` program does exactly that: turn the red lamp on and the green one off, wait, then swap them, wait again. Watched on its own, it looks a lot like two rates. It is not: both lamps are locked to the same half-second wait, because there is only one wait in the whole sketch. Try to give the green lamp a rate that does not divide evenly into the red one's, without touching `millis()`, and there is no way to write it: a third `delay()` call can only run once the other two have finished, so a third independent rate is not one step harder, it is impossible in this shape.

## Reading a button on the same loop

The real payoff of this pattern shows up once a sketch needs to do more than blink. A `loop()` built around `delay()` cannot check a button, print to the serial monitor or update a display while it is waiting out a blink; a `loop()` built around `millis()` checks are all just more `if` blocks that run every single pass, so a button read sits comfortably alongside two independent blink timers with nothing about either one changing. This is also the shape the page on debouncing a button uses for its own fix, which is not a coincidence: it is the same problem, timing something without blocking everything else, solved the same way.

## Questions

What happens when millis() rolls over after 49.7 days?

The unsigned subtraction in now - lastTime wraps correctly and keeps producing the right interval, which is the entire reason the pattern is written as a subtraction rather than as lastTime + INTERVAL compared to now.

Is delay() ever fine to use?

Yes, for a sketch that genuinely has nothing else to do while it waits, or for a very short wait in setup(). The moment a second independent timer, a button read or a serial check needs to happen during that wait, delay() is the wrong tool.

How do I add a third timer at a third rate?

Add a third lastTime variable, a third interval and a third if block, exactly the shape of the two already there. Nothing about the pattern changes as more timers join it, which is the point of writing it this way.

Does using interrupts replace this pattern?

They solve a different problem: an interrupt reacts to an event (an edge, a timer overflow) the instant it happens, while millis() timing is for jobs that only need to happen on a schedule loop() can check on its own. Many sketches use both.

Why use unsigned long instead of int for the timing variables?

millis() returns an unsigned long because the count needs more range than a 16-bit int gives on an Uno; storing it in a smaller or signed type truncates or misreads the value long before the 49.7-day rollover ever becomes the issue.

What is micros() for, if millis() already exists?

Finer resolution, in microseconds rather than milliseconds, for timing that needs to be tighter than a millisecond matters for, at the cost of rolling over far sooner, roughly every 71 minutes rather than every 49.7 days. The same subtraction pattern above works for it unchanged.

Related

## Keep going

PWM: Faking an Analog Voltage on a Digital Pin Arduino State Machine: A Four-State Reaction Game Interrupts vs Polling on Arduino, Side by Side The Arduino Uno simulator The same millis() pattern, for debouncing a button A third use for a running clock: fading an LED

## Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.

Start building Open the editor
