ESP32/Living on a network/47. Keeping time with time.h
Your chip
Your language
Living on a network · 47 of 81

Keeping time with time.h

The ESP32 has no battery-backed clock, so every boot starts in 1970 until something tells it otherwise. The C library does the rest — including the leap years you were about to compute by hand.

/esp32/keeping-time-with-time-h · arduino · S3

One number, seven fields

time_t → struct tm
synced
Timezone passed to configTime()
Has NTP answered yet?
Local time
10:26:40
Date
10 Sep 2026
Offset
+10 h
One number, seven fields, no arithmetic of yours. The timezone is an offset handed to configTime(), not something you add afterwards — do it by hand and you get the hour right and daylight saving wrong twice a year.

time_t is one integer: seconds since 1 January 1970 UTC. localtime_r turns it into a struct tm with the year, month, day, hour, minute, second and weekday already correct, leap years and all.

Everybody writes that arithmetic by hand once — divide by 86400, add four for the weekday, subtract the days in each month — and everybody gets a leap year or a century rule wrong. The library has been right about it since before this chip existed.

Use a timezone, not an offset

The common recipe passes gmtOffset_sec and daylightOffset_sec to configTime, which is a fixed choice: the board is either on summer time or it is not, forever, until you reflash it.

configTzTime takes a POSIX timezone string instead. AEST-10AEDT,M10.1.0,M4.1.0/3 says the offset, the summer offset, and the two dates it changes on — so the board handles it itself, in the middle of the night, in the right week.

Check the year, not the return value

getLocalTime() returns true whenever there is a time, and after a cold boot there always is one: zero, which is 1970. The only reliable test is whether the year looks plausible.

if (t.tm_year + 1900 < 2020) { /* not synced yet */ }

Anything that timestamps a reading, checks a certificate, or decides whether a schedule has passed needs this guard, and the failures without it are subtle — a log full of 1970, or a TLS handshake that fails with a date error nobody connects to the clock.

Across a deep sleep

The RTC keeps counting while the chip sleeps, so the time survives a wake. It does not survive a power cut, and it drifts more in sleep than awake. A board that wakes every ten minutes for a year should re-sync occasionally — once a day is plenty, and it costs one UDP exchange.

On your S3
ChipXtensa LX7 · 2 × 240 MHz
Board settingESP32S3 Dev Module
Default I2CSDA 8 · SCL 9
Watch out forThe port vanishes after upload

The code

configTime starts the sync in the background and returns immediately. Everything after it is standard C, and the POSIX timezone string handles daylight saving without you.

clock.ino
#include <WiFi.h>
#include <time.h>

// POSIX TZ: rules, not a fixed offset — daylight saving is handled for you.
const char *TZ_MELBOURNE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

void setup() {
  Serial.begin(115200);
  WiFi.begin("your-ssid", "your-password");
  while (WiFi.status() != WL_CONNECTED) delay(250);

  configTzTime(TZ_MELBOURNE, "pool.ntp.org", "time.nist.gov");
}

void loop() {
  struct tm t;
  if (!getLocalTime(&t, 100) || t.tm_year + 1900 < 2020) {
    Serial.println("no time yet");
  } else {
    char buf[32];
    strftime(buf, sizeof buf, "%a %d %b %Y  %H:%M:%S", &t);
    Serial.println(buf);
  }
  delay(1000);
}

The year check is the real test for "is the time valid". getLocalTime returns true as soon as there is any time at all, and on a cold boot that time is January 1970.

When it does not work

The date says 1 January 1970

The sync has not landed yet, or it never will. configTime returns immediately and the answer arrives a second or two later over UDP. Check that the year is past 2020 before you log or timestamp anything.

The time is right and the hour is wrong

A fixed offset instead of a timezone. gmtOffset plus daylightOffset is correct twice a year and wrong the rest of the time. A POSIX TZ string carries the switch dates, so the board changes on its own.

It syncs on the bench and not on the customer network

NTP is UDP port 123 outbound, and plenty of corporate and guest networks block it. Ask the router instead — most of them run an NTP server on the gateway address — or fall back to an HTTPS Date header.

The clock drifts a few seconds a day

That is the internal oscillator, and it is normal. The default sync interval is an hour; shorten it with sntp_set_sync_interval, or accept the drift, which for logging almost always does not matter.

Where this goes next

The next thing that needs a correct clock. A certificate has a validity window, and a board that thinks it is 1970 fails every check in it.

HTTPS and certificates

Edit this page — content/esp32/keeping-time-with-time-h.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32 on the forum