Using delay() Breaks Buttons, Sensors, Displays and Communication

Many Arduino projects start with delay(). It is simple, easy to understand, and useful for a first blinking LED sketch. But as soon as a project grows beyond one simple task, delay() becomes one of the most common reasons why buttons do not respond, displays update slowly, sensors are missed, and communication feels unreliable.

The problem is not that delay() is always bad. The problem is that delay() stops the sketch from doing anything else while it waits.

If your project needs to read buttons, update a display, check sensors, control LEDs, receive serial data, or handle WiFi while also waiting for something, blocking delays can make the whole project feel broken.

Typical Symptoms

  • A button only works if it is pressed at exactly the right moment.
  • The display freezes during animations or waiting periods.
  • Sensor readings update too slowly.
  • Serial commands are missed.
  • A menu feels laggy or unresponsive.
  • A relay or LED stays on longer than expected.
  • WiFi or web server response becomes slow.
  • Several project features work alone, but not together.

What delay() Actually Does

The delay() function pauses the sketch for a specified number of milliseconds.

delay(1000);

This waits for one second. During that second, the sketch does not continue to the next line of code.

That is fine for a simple blink example:

digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);

But if the project also needs to check a button during that delay, the button will not be checked until the delay is over.

Common Cause: Button Presses Are Missed

A push button may be pressed for only a short time. If the sketch is inside a long delay while the button is pressed and released, the code never sees it.

Example problem:

digitalWrite(LED_PIN, HIGH);
delay(5000);
digitalWrite(LED_PIN, LOW);

if (digitalRead(BUTTON_PIN) == LOW) {
  Serial.println("Button pressed");
}

In this example, the button is only checked after the five-second delay. Any press during the delay is ignored.

This is why beginners often think the button, wiring or pull-up resistor is unreliable when the real problem is blocking code.

Common Cause: Display Updates Freeze

Displays only update when the sketch sends new information to them. If the code is stuck in delay(), the display cannot update.

This can cause:

  • Clock displays that pause during other actions.
  • Menus that respond slowly.
  • Progress indicators that freeze.
  • OLED animations that block button input.
  • LCD messages that stay on screen too long.

The display is not necessarily slow. The sketch is simply not reaching the display update code often enough.

Common Cause: Sensor Timing Becomes Wrong

Some sensors need to be read at specific intervals. Others are slow and should not be read too often. Blocking delays make it harder to manage timing cleanly when several sensors are involved.

Examples:

  • DHT22 should not be read too frequently.
  • Analog sensors may need averaging over time.
  • Motion sensors may need frequent checks.
  • Rotary encoders may be missed if checked too rarely.
  • Ultrasonic sensors can block if no echo is received unless timeouts are used.

A single delay(2000) for one sensor may make another part of the project unresponsive.

Common Cause: Serial Data Is Missed or Delayed

Serial data can arrive while the sketch is delayed. The serial buffer may hold some data, but if data arrives faster than it is processed, the buffer can overflow or commands may be handled late.

This matters for:

  • Serial command interfaces.
  • GPS modules.
  • Bluetooth serial modules.
  • RS485 communication.
  • MIDI input.
  • Debug commands from the serial monitor.

If a project uses serial communication, avoid long blocking delays.

Common Cause: WiFi and Web Projects Become Unresponsive

On ESP32 and ESP8266 projects, long delays can make web servers, WiFi tasks and communication feel slow. Some background system tasks still run, but your application code is not checking clients, updating states or responding quickly.

Symptoms include:

  • Web page loads slowly.
  • Button press on web interface reacts late.
  • MQTT messages are delayed.
  • Sensor updates stop during long waits.
  • The watchdog may reset the board if code blocks badly enough.

For connected projects, blocking code is often the difference between a responsive device and a frustrating one.

The Better Idea: Check Time Without Stopping the Sketch

Instead of stopping the sketch with delay(), let the loop continue running and check whether enough time has passed.

This is usually done with millis().

millis() returns the number of milliseconds since the board started running the sketch.

unsigned long now = millis();

The basic idea is:

  • Remember when something last happened.
  • Keep running the loop.
  • Check whether the desired time interval has passed.
  • If yes, do the action and update the timestamp.

Blink Without delay()

This is the classic example of non-blocking timing.

const int ledPin = LED_BUILTIN;

unsigned long previousMillis = 0;
const unsigned long interval = 1000;

bool ledState = false;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    ledState = !ledState;
    digitalWrite(ledPin, ledState);
  }

  // Other code can run here all the time.
}

The LED still blinks once per second, but the sketch is not frozen between changes. Other code can run during the waiting time.

Button Check While LED Blinks

With non-blocking timing, a button can be checked continuously while the LED timing runs independently.

const int ledPin = LED_BUILTIN;
const int buttonPin = 2;

unsigned long previousMillis = 0;
const unsigned long interval = 1000;

bool ledState = false;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT_PULLUP);
  Serial.begin(115200);
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;

    ledState = !ledState;
    digitalWrite(ledPin, ledState);
  }

  if (digitalRead(buttonPin) == LOW) {
    Serial.println("Button is pressed");
  }
}

Now the button is checked every time the loop runs, not only after a delay ends.

Common Cause: Replacing One delay() with Another delay()

Some sketches remove one long delay but still contain other blocking code elsewhere. The project may still feel broken.

Look for blocking functions such as:

  • delay()
  • Long while loops
  • Waiting forever for WiFi connection
  • Waiting forever for serial input
  • Sensor functions without timeout
  • Pulse measurements without timeout
  • Animations that run inside a blocking loop

A project becomes responsive only when the main loop can keep cycling quickly.

Common Cause: Blocking while Loops

A while loop can be just as blocking as delay().

while (digitalRead(BUTTON_PIN) == HIGH) {
  // Wait here until button is pressed
}

This code waits in one place and prevents the rest of the project from running.

A better structure is to check the button state and then continue:

if (digitalRead(BUTTON_PIN) == LOW) {
  Serial.println("Button pressed");
}

The main loop should keep moving.

Common Cause: Waiting Forever for WiFi

Many WiFi examples wait until the connection succeeds:

while (WiFi.status() != WL_CONNECTED) {
  delay(500);
}

This is okay for a short demo, but not great for a real project. If WiFi is unavailable, the project may never continue to sensor reading, display updates or local control.

A better approach is to use a timeout:

unsigned long startAttempt = millis();

while (WiFi.status() != WL_CONNECTED && millis() - startAttempt < 10000) {
  delay(250);
}

if (WiFi.status() == WL_CONNECTED) {
  Serial.println("WiFi connected");
} else {
  Serial.println("WiFi timeout, continuing offline");
}

This still uses a small delay inside the WiFi wait loop, but it avoids waiting forever. For more advanced projects, WiFi connection handling can be made fully non-blocking.

Common Cause: Blocking Display Animations

Many display examples use loops and delays to animate text, graphics or scrolling effects. That is fine for a demo, but it can break real projects.

Example problem:

for (int x = 0; x < 128; x++) {
  drawAnimationFrame(x);
  delay(50);
}

During this animation, buttons, sensors and communication are not handled unless the code inside the loop also checks them.

For real projects, animations should update one step at a time and then return to the main loop.

Simple Task Timing Pattern

A practical project often has several timed tasks:

  • Read buttons every loop.
  • Update display every 200 ms.
  • Read sensor every 2 seconds.
  • Blink status LED every 1 second.
  • Send data every 60 seconds.

Each task can have its own timestamp.

unsigned long lastDisplayUpdate = 0;
unsigned long lastSensorRead = 0;
unsigned long lastDataSend = 0;

void loop() {
  unsigned long now = millis();

  // Buttons can be checked every loop
  checkButtons();

  if (now - lastDisplayUpdate >= 200) {
    lastDisplayUpdate = now;
    updateDisplay();
  }

  if (now - lastSensorRead >= 2000) {
    lastSensorRead = now;
    readSensor();
  }

  if (now - lastDataSend >= 60000) {
    lastDataSend = now;
    sendData();
  }
}

This structure keeps the project responsive because no task blocks the entire sketch for a long time.

Debouncing Buttons Without Long delay()

Button contacts bounce mechanically. A short delay is often used to debounce buttons, but long debounce delays make menus feel sluggish.

A better method is to record when the button changed and only accept the new state after it remains stable for a short time.

For many projects, a debounce time of about 20 ms to 50 ms is enough. The important part is not to freeze the whole sketch for that time.

When delay() Is Still Acceptable

delay() is not forbidden. It is fine in simple cases.

Examples where delay() may be acceptable:

  • Very simple Blink examples.
  • Short startup delays.
  • Small pauses in test sketches.
  • Waiting a few milliseconds for hardware to settle.
  • Simple one-purpose projects with no buttons, communication or multitasking.

The problem starts when a project needs to do more than one thing at a time from the user’s point of view.

How to Tell If delay() Is Your Problem

Ask these questions:

  • Does the project ignore button presses sometimes?
  • Does the display freeze during another operation?
  • Does communication respond late?
  • Does one part of the sketch wait while another part should still work?
  • Are there delays longer than 100 ms in the main loop?

If yes, blocking code is probably part of the problem.

Recommended Troubleshooting Steps

  1. Search the sketch for delay().
  2. Also search for blocking while and for loops.
  3. Temporarily reduce long delays and see if buttons or displays improve.
  4. Replace simple timing delays with millis() checks.
  5. Make each task run quickly and return to loop().
  6. Add timeouts to WiFi, sensor and serial waits.
  7. Update animations one frame at a time.
  8. Read buttons frequently and debounce without freezing the sketch.
  9. Test each function separately, then combine them with non-blocking timing.
  10. Keep the main loop moving as quickly as practical.

Quick Diagnostic Table

Symptom Likely Cause First Thing to Try
Button only works sometimes Button checked too rarely because of delays Check button every loop and use millis-based debounce
Display freezes during waiting Display update blocked by delay or loop Update display on a timed interval without blocking
Serial command responds late Serial input not checked while delayed Read serial frequently and avoid long delay()
WiFi project becomes unresponsive Blocking network or wait code Add timeouts and avoid waiting forever
Animation blocks everything else Animation loop contains delay() Draw one animation frame per loop cycle
Several features work alone but fail together Blocking code prevents cooperative timing Convert tasks to millis-based state machines

What Not to Do

  • Do not use long delay() calls in projects with buttons or menus.
  • Do not wait forever for WiFi, sensors or serial input.
  • Do not put long animations in blocking loops if the project must remain responsive.
  • Do not assume a missed button press is always a wiring problem.
  • Do not combine many demo sketches full of delays and expect them to multitask.
  • Do not rewrite hardware before checking whether the sketch is simply blocked.

CANABLOX Practical Note

CANABLOX makes it easy to combine controllers, displays, sensors, keypads, RTCs, ADCs and other modules into one project. That also means the sketch often needs to handle several things at once from the user’s point of view.

For CANABLOX projects, avoid long blocking delays once buttons, displays or multiple modules are involved. A clean modular hardware system works best with clean non-blocking software structure: read inputs often, update displays on intervals, read sensors when due, and keep the main loop moving.

Conclusion

delay() is useful for simple examples, but it becomes a problem when a project needs to respond to buttons, update displays, read sensors or communicate while waiting.

If a project feels slow, misses input or freezes during other actions, look for blocking delays and loops before blaming the hardware.

Replacing long delays with millis()-based timing is one of the most important steps from beginner Arduino sketches toward reliable real-world projects.

Shopping Cart
Scroll to Top