All posts
esp32arduinoarchitecture

How to use ESP32 timers: stop using delay() in your firmware

· 8 min read NEW

Every beginner learns delay(). Every professional stops using it.

Here’s why — and what to do instead.

The problem with delay()

When you call delay(1000), the ESP32 does absolutely nothing for one second. No sensor reads. No button checks. No MQTT messages processed. If you’re blinking an LED, that’s fine. If you’re building real firmware, it’s a disaster.

// This firmware is broken for any real use case
void loop() {
  digitalWrite(LED_PIN, HIGH);
  delay(1000);               // nothing else runs for 1 second
  digitalWrite(LED_PIN, LOW);
  delay(1000);               // nothing else runs for another second
}

The moment you add a second feature — reading a button, checking a sensor, handling MQTT — delay() makes the whole system sluggish or completely unresponsive.

Solution 1: millis()-based timing

The simplest fix. Track time with millis() instead of blocking.

#define LED_PIN     2
#define BLINK_MS  500

unsigned long lastBlink = 0;
bool ledState = false;

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

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

  if (now - lastBlink >= BLINK_MS) {
    lastBlink = now;
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
  }

  // Everything else runs here freely — no blocking
  readSensors();
  handleMQTT();
  checkButtons();
}

loop() runs hundreds of times per second. Each feature checks if enough time has passed and acts accordingly. Nothing blocks anything else.

Solution 2: ESP32 hardware timers

For precision timing — pulse generation, encoder reading, exact intervals — use the ESP32’s hardware timers directly.

#include "esp_timer.h"

#define TIMER_INTERVAL_US  500000   // 500ms in microseconds

esp_timer_handle_t blinkTimer;

void IRAM_ATTR onTimer(void* arg) {
  // Runs in ISR context — keep it short
  static bool state = false;
  state = !state;
  digitalWrite(LED_PIN, state);
}

void setup() {
  pinMode(LED_PIN, OUTPUT);

  esp_timer_create_args_t args = {
    .callback = onTimer,
    .name     = "blink"
  };
  esp_timer_create(&args, &blinkTimer);
  esp_timer_start_periodic(blinkTimer, TIMER_INTERVAL_US);
}

void loop() {
  // LED blinks independently of everything in loop()
}

Hardware timers fire with microsecond precision regardless of what loop() is doing.

Solution 3: Ticker library (easiest)

If you don’t want to touch the ESP-IDF timer API directly, the Ticker library wraps it cleanly:

#include <Ticker.h>

Ticker blinkTicker;
Ticker sensorTicker;

void blinkLED() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

void readSensor() {
  float temp = readTemperature();
  Serial.println(temp);
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  blinkTicker.attach(0.5, blinkLED);    // every 500ms
  sensorTicker.attach(5.0, readSensor); // every 5s
}

void loop() {
  // Completely free for other work
}

Two independent timers, zero blocking, clean code.

Which approach to use

SituationUse
Simple multi-task firmwaremillis() pattern
Precise intervals (< 1ms)Hardware timer via esp_timer
Quick setup, readable codeTicker library
Real-time OS neededFreeRTOS tasks (see next post)

The rule

If you find yourself writing delay() in any firmware that does more than one thing, stop and use one of these patterns instead. Your firmware will be more responsive, more reliable, and easier to extend.

In the next post we’ll look at FreeRTOS tasks — the right tool when you need true parallel execution on the ESP32’s dual cores.

Related posts

esp32architecture
Jun 19, 2026
esp32arduino
Jun 19, 2026
esp32architecture
Jun 19, 2026

Comments

Enjoyed this tutorial?

Get new ESP32, Arduino, and industrial IoT tutorials straight to your inbox — no spam, unsubscribe anytime.