Zbotic Logo Zbotic Logo
  • Home
  • Shop
  • Sale
  • 3D Print Service
  • PCB Service
  • B2B
  • Blogs
  • Contact Us
0 0

View Wishlist Add all to cart

0 0
0 Shopping Cart
Shopping cart (0)
Subtotal: ₹0.00

View cartCheckout

  • Shop
  • About Us
  • Contact Us
  • Reseller
  • Blogs
020 69134444
1800 209 0998
[email protected]
Help Desk
Facebook Twitter Instagram Linkedin YouTube
Zbotic Logo Zbotic Logo
0 0

View Wishlist Add all to cart

0 0
0 Shopping Cart
Shopping cart (0)
Subtotal: ₹0.00

View cartCheckout

All departments
  • 3D Print Service
  • 3D Printer
  • Batteries & Chargers
  • Development Boards
  • Drone Parts
  • EBike parts
  • Sensor Modules
  • Electronic Components
  • Electronic Modules
  • IoT and Wireless
  • Mechanical Parts and Workbench Tools
  • Motors & Drivers & Pumps & Actuators
  • DIY and Robot Kits
  • Show more
  • Home
  • Shop
  • Sale
  • 3D Print Service
  • PCB Service
  • B2B
  • Blogs
  • Contact Us
Return to previous page
Home Arduino & Microcontrollers

Arduino Timer Library: millis vs delay Explained

Arduino Timer Library: millis vs delay Explained

March 11, 2026 /Posted byJayesh Jain / 0

Every Arduino project needs to deal with time. Whether you are blinking an LED, reading a sensor every 500 ms, debouncing a button, or running multiple tasks simultaneously — you need precise, reliable timing. Most beginners start with delay(). Most experienced developers rarely use it. This guide explains why, and how to write Arduino code that handles time correctly without blocking your program’s execution.

Table of Contents

  1. What Is delay() and What Is It Doing to Your Code?
  2. millis() Explained: The Non-Blocking Alternative
  3. The millis() Pattern: Non-Blocking Timing in Practice
  4. micros() for Microsecond Precision
  5. Handling millis() Overflow Correctly
  6. Multitasking with millis(): Running Multiple Timers
  7. Timer Libraries: SimpleTimer, TaskScheduler, and More
  8. Frequently Asked Questions

What Is delay() and What Is It Doing to Your Code?

delay(ms) pauses the entire program for the specified number of milliseconds. During this time, nothing else runs. No sensor readings, no button checks, no serial communication — the microcontroller simply sits in a busy wait loop counting clock cycles.

void loop() {
  digitalWrite(LED_PIN, HIGH);
  delay(1000);  // Arduino is completely frozen for 1 second
  digitalWrite(LED_PIN, LOW);
  delay(1000);  // Frozen again
  // If a button is pressed during any delay, it WILL be missed
}

delay() is implemented using Timer0, which runs at 16 MHz and overflows every 64 microseconds. The delay() function spins in a loop checking this counter until the specified time has elapsed.

When is delay() acceptable?

  • In setup(), to give external hardware time to initialise (e.g., delay(100) after power-on).
  • One-shot sequences where you genuinely want everything to stop (e.g., startup animation).
  • Very simple single-task sketches with no responsiveness requirements.

When does delay() fail you?

  • Any project with more than one independently timed task.
  • Projects that must respond to button presses, encoder inputs, or serial commands at any time.
  • Projects driving stepper motors or other hardware that needs continuous background attention.
Recommended: Arduino Starter Kit with 170 Pages Project Book — The perfect learning kit for building from simple delay-based projects to more sophisticated millis()-based designs, with guided project examples for each skill level.

millis() Explained: The Non-Blocking Alternative

millis() returns the number of milliseconds that have elapsed since the Arduino was last powered on or reset. It is backed by Timer0 and updates via a hardware interrupt every 64 microseconds (Timer0 overflow), providing millisecond-resolution timing.

unsigned long startTime = millis();
// ... do other things ...
if (millis() - startTime >= 1000) {
  // 1 second has passed
}

Key properties of millis():

  • Returns unsigned long (32-bit, 0 to 4,294,967,295).
  • Overflows (wraps back to 0) after approximately 49.7 days.
  • Accuracy: typically plus or minus 1 ms per reading; slightly affected by long ISRs.
  • Resolution: 1 ms on standard 16 MHz boards.
  • Does not block — calling millis() takes only a few microseconds and your code continues immediately.
Recommended: Arduino Uno R3 Beginners Kit — Comprehensive kit including the Arduino Uno R3 and components needed to practice millis()-based LED control, button debouncing, and multi-tasking exercises.

The millis() Pattern: Non-Blocking Timing in Practice

The standard pattern stores the last time an action occurred in a previousMillis variable, then compares the current time against it on every loop iteration:

unsigned long previousMillis = 0;
const unsigned long INTERVAL = 1000;  // 1 second
bool ledState = LOW;

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

  if (currentMillis - previousMillis >= INTERVAL) {
    previousMillis = currentMillis;
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);
  }

  // Everything here runs on EVERY loop iteration — nothing is blocked!
  checkButton();
  readSensor();
  updateDisplay();
}

Notice that previousMillis = currentMillis is used (not previousMillis += INTERVAL). Both approaches work, but they have different behaviour under load:

  • previousMillis = currentMillis — If a loop iteration takes longer than expected and the interval is exceeded, the next interval starts from the current time. This prevents cascading catch-up.
  • previousMillis += INTERVAL — Keeps intervals precisely equal even if individual iterations are slow. Use this when strict period accuracy matters (e.g., data logging at exact 1s intervals).

micros() for Microsecond Precision

micros() returns the number of microseconds since last reset. It provides 4 microsecond resolution on 16 MHz boards (8 microseconds on 8 MHz boards like the Pro Mini 3.3V).

// Measure pulse width with microsecond precision
unsigned long pulseStart = micros();
while (digitalRead(ECHO_PIN) == HIGH);
unsigned long pulseWidth = micros() - pulseStart;
// Convert to distance: pulseWidth / 58 = cm

micros() overflows after approximately 70 minutes (2^32 microseconds). The same overflow-safe subtraction technique used for millis() works identically for micros().

Recommended: Arduino Frequency Counter Kit with 16×2 LCD Display — A ready-made project demonstrating advanced timer use — uses Timer1 in input capture mode for precise frequency measurement far beyond what millis() or micros() can achieve.

Handling millis() Overflow Correctly

After 49.7 days, millis() wraps from 4,294,967,295 back to 0. In reality, the subtraction pattern handles overflow automatically due to unsigned arithmetic wraparound:

Suppose millis() just wrapped: currentMillis = 100, and previousMillis was set just before the wrap: previousMillis = 4294967200. The subtraction 100 – 4294967200 using 32-bit unsigned arithmetic equals 196 ms, which is the correct elapsed time.

This works perfectly as long as:

  1. Both variables are declared as unsigned long — not int or long.
  2. You use subtraction (currentMillis – previousMillis), not comparison (currentMillis > previousMillis + INTERVAL — this breaks at overflow).
// WRONG — breaks when millis() overflows!
if (millis() > previousMillis + INTERVAL) { ... }

// CORRECT — always works
if (millis() - previousMillis >= INTERVAL) { ... }

Multitasking with millis(): Running Multiple Timers

The power of millis() becomes clear when you run multiple independent tasks in the same sketch — something impossible with delay():

// Task 1: Blink LED every 500ms
unsigned long led_prev = 0;
const unsigned long LED_INTERVAL = 500;
bool ledState = LOW;

// Task 2: Read sensor every 2 seconds
unsigned long sensor_prev = 0;
const unsigned long SENSOR_INTERVAL = 2000;

// Task 3: Check button every 50ms
unsigned long btn_prev = 0;
const unsigned long BTN_INTERVAL = 50;

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

  if (now - led_prev >= LED_INTERVAL) {
    led_prev = now;
    ledState = !ledState;
    digitalWrite(LED_BUILTIN, ledState);
  }

  if (now - sensor_prev >= SENSOR_INTERVAL) {
    sensor_prev = now;
    float temp = readTemperature();
    Serial.println(temp);
  }

  if (now - btn_prev >= BTN_INTERVAL) {
    btn_prev = now;
    int btnState = digitalRead(BTN_PIN);
    if (btnState == LOW) onButtonPress();
  }
}

This is cooperative multitasking — each task yields control back to the main loop after doing its work. The key requirement is that each task’s work takes a short, bounded time. Tasks that take more than 10 ms will cause jitter in other tasks’ timing.

Recommended: Multifunction Shield For Arduino Uno / Leonardo — Multiple outputs (4-digit display, 3 LEDs, buzzer, 3 buttons) in one shield — ideal for practising millis()-based multitasking where each component runs on its own independent timer.

Timer Libraries: SimpleTimer, TaskScheduler, and More

For more complex projects, libraries abstract the millis() pattern into cleaner APIs:

TaskScheduler: The most capable and actively maintained option. Supports task enable/disable, finite run counts, callbacks, priority, and sleep modes.

#include <TaskScheduler.h>

void blinkCallback();
void sensorCallback();

Task tBlink(500, TASK_FOREVER, &blinkCallback);
Task tSensor(2000, TASK_FOREVER, &sensorCallback);

Scheduler runner;

void blinkCallback() {
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void sensorCallback() {
  Serial.println(analogRead(A0));
}

void setup() {
  Serial.begin(9600);
  runner.init();
  runner.addTask(tBlink);
  runner.addTask(tSensor);
  tBlink.enable();
  tSensor.enable();
}

void loop() {
  runner.execute();
}

TimerOne / TimerThree libraries: These control hardware Timer1 or Timer3 directly, providing interrupt-driven callbacks at precise intervals — not subject to loop() jitter. Useful for generating audio tones, precise PWM, or stepper motor control.

#include <TimerOne.h>

void timerISR() {
  // Called EXACTLY every 1ms regardless of what loop() is doing
  stepperDriver.tick();
}

void setup() {
  Timer1.initialize(1000);  // 1000 us = 1ms interval
  Timer1.attachInterrupt(timerISR);
}
Recommended: Arduino Nano Every with Headers — ATmega4809 features an enhanced Timer/Counter type B (TCB) with 16-bit resolution and separate input capture, perfect for advanced timer library experiments and high-precision timing applications.

Frequently Asked Questions

Does millis() drift over time and become inaccurate?

The accuracy of millis() depends on the resonator or crystal on your board. Arduino Uno uses a ceramic resonator with plus or minus 0.5% accuracy, meaning after 1 hour it could be off by plus or minus 18 seconds. Arduino boards that use a crystal oscillator (rather than resonator) are typically accurate to plus or minus 20 ppm — about plus or minus 1.7 seconds per day. For applications needing real-time accuracy, add an external RTC module (DS3231 is plus or minus 2 ppm) and periodically re-sync your timing.

Can I use delay() and millis() in the same sketch?

Yes. delay() internally uses millis() and does not corrupt any state. You can freely mix them. The practical concern is that delay() blocks your sketch while millis()-based code in other parts of the loop cannot run. Many developers use delay() in setup() for hardware initialisation delays, and millis() everywhere in loop().

Why does my millis()-based timer fire slightly late sometimes?

The millis() pattern only checks timing on each pass through loop(). If some other part of your loop takes 50 ms (perhaps a slow I2C read or serial print), your 100 ms timer will fire at 150 ms instead of 100 ms. To fix this: ensure no single task in your loop blocks for more than a few milliseconds, avoid delay() anywhere in loop(), and consider moving critical timing to hardware timer interrupts using the TimerOne library.

What is the difference between millis() and micros() for timing?

millis() has 1 ms resolution and overflows every 49.7 days — suitable for most task scheduling, debouncing, and periodic operations. micros() has 4 microsecond resolution and overflows every 70 minutes — suitable for measuring short pulses (ultrasonic sensors, IR signals), generating precise waveforms, or profiling code execution time. Use micros() when you need finer granularity than 1 ms, but watch for the much sooner overflow.

How do I run a task exactly N times and then stop?

Track a counter alongside your timer. Increment it each time the task fires, and stop executing when it reaches the maximum. The TaskScheduler library provides a built-in mechanism for this: Task tMyTask(100, 5, &callback) where 5 is the number of iterations before automatic disabling.


Take your Arduino projects from blink to multitasking.
Browse our complete range of Arduino boards and kits at Zbotic.in — genuine products, expert support, fast delivery across India.
Tags: Arduino, delay, millis, multitasking, non-blocking, programming, timer, timing
Share Post
  • Facebook
  • Linkedin
  • Whatsapp
IoT Dashboard with Grafana and...
blog iot dashboard with grafana and influxdb on local network 595398
blog esp32 captive portal wi fi credentials entry without code 595401
ESP32 Captive Portal: Wi-Fi Cr...

Related posts

Svg%3E
Read more

Arduino Batch Programming: Flash Multiple Boards Quickly

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Based Radar System with Ultrasonic Sensor

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Automatic Plant Monitor: Sunlight, Moisture, Temperature

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Lie Detector: GSR Sensor Polygraph Project

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading
Svg%3E
Read more

Arduino Metal Detector: Build a Treasure Finder

April 1, 2026 0
Table of Contents Introduction Components and Hardware Setup Wiring Diagram and Connections Complete Code with Explanation Customization and Improvements Troubleshooting... Continue reading

Add comment Cancel reply

Your email address will not be published. Required fields are marked

Facebook Twitter Instagram Pinterest Linkedin Youtube

Get the latest deals and more.

Download on Google Play Download on the App Store

Call us: 020 69134444 / 1800 209 0998

Monday - Saturday 09:30 AM - 06:00 PM
For Technical Supports Email: [email protected]
For Sales / Enquiries Email: [email protected]

  • My Account

    • Cart

    • Wishlist

    • Checkout

    • My Orders

    • Track Order

    • My Account

  • Information

    • FAQs

    • Blogs

    • Career

    • About Us

    • Contact Us

    • Payment Options

  • Policies

    • Privacy Policy

    • Terms & Conditions

    • GST Input Tax Credit

    • Shipping Return Policy

    • E-Waste Collection Points

    • Our Sitemap

© Zbotic.in is registered trademark of Moxie Supply Pvt Ltd – All Rights Reserved
Login
Use Phone Number
Use Email Address
Not a member yet? Register Now
Reset Password
Use Phone Number
Use Email Address
Register
Already a member? Login Now