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.
One number, seven fields
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.
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.
#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.
ntptime.settime() sets the RTC to UTC, once. Everything after that is arithmetic on a tuple, and the timezone is yours to add.
import network, ntptime, time
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect("your-ssid", "your-password")
while not wlan.isconnected():
time.sleep(0.25)
ntptime.settime() # sets the RTC to UTC
OFFSET = 10 * 3600 # AEST; no DST handling here
while True:
y, mo, d, h, mi, s, wd, _ = time.localtime(time.time() + OFFSET)
print("%04d-%02d-%02d %02d:%02d:%02d" % (y, mo, d, h, mi, s))
time.sleep(1)MicroPython's ntptime has no daylight-saving rules at all. If that matters, either compute the two switch dates yourself or keep the board on UTC and convert wherever the reading is displayed.
When it does not work
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.
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.
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.
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.
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.