Note: This tutorial explains how to get accurate time from the internet using NTP (Network Time Protocol) via WiFi. This is a common need for ESP32 — the device has no clock battery, so time is lost on power off and must be re-synced every boot. Please complete the STA mode (WiFi connection) tutorial first.
In daily use, you may encounter these scenarios:
But ESP32 has a fundamental problem: It has no battery-backed clock (RTC). Time is lost on power off, and every boot starts from an old default value (could be 1970 or January 1, 2024).
Simple analogy: ESP32 is like a person without a watch — every time they wake up, they don't know what time it is. NTP is the mechanism to "ask the internet server: what time is it?". As long as you connect to WiFi, you can get accurate time from the internet.
This tutorial's goal: ESP32 connects to WiFi → gets time from NTP server → sets timezone → prints current time to serial every second.
NTP (Network Time Protocol) is a protocol for synchronizing computer system clocks over a network. There are thousands of NTP servers worldwide that anyone can query for free.
Workflow:
┌────────┐ NTP query request ┌──────────────┐
│ ESP32 │ ─────────────────────────► │ NTP Server │
│ │ "What time is it?" │ (e.g., │
│ │ │ ntp.aliyun.com) │
└────────┘ └──────┬───────┘
▲ │
│ NTP time response │ Returns precise time
│ "Current time: 2026-08-10 14:30:00" │
└──────────────────────────────────────────────┘
Unlike computers and phones, ESP32 has no battery-backed hardware clock (RTC). This means:
Key understanding: NTP sync is not instantaneous! After ESP32 sends a request, it must wait for the server response before time becomes valid. This can take 1-10 seconds depending on network conditions.
A Timezone is a time standard for different regions on Earth. The world is divided into 24 timezones, each differing by 1 hour.
Simple analogy: When Beijing is 3 PM, London is 7 AM and New York is 2 AM. This is because they're in different timezones.
ESP32's default time from NTP is UTC (Coordinated Universal Time), and we need to set the timezone to see local time.
ESP32 uses POSIX standard timezone strings to set the timezone. Format:
StandardTimezoneName[UTCoffset][DaylightSaving[offset,start,end]]
Common timezone strings:
| Region | TZ String | Meaning |
|---|---|---|
| China | CST-8 |
UTC+8, no DST |
| Japan | JST-9 |
UTC+9, no DST |
| UK (winter) | GMT0 |
UTC+0, no DST |
| US Eastern | EST+5 |
UTC-5, no DST |
| US Pacific | PST+8PDT,M3.2.0,M11.1.0 |
UTC-7 with DST Mar-Nov |
Meaning of each part (using CST-8 as example):
CST: Timezone name (China Standard Time)-8: Difference from UTC. Note the sign direction: -8 means UTC+8 (East 8), +5 means UTC-5 (West 5)Beginner pitfall: The sign is easy to get backwards! China is UTC+8, but the TZ string uses
CST-8(negative sign). Think of it this way: UTC time plus the offset equals local time, soCST-8means "local time = UTC + 8 hours".
| Server Address | Location | Notes |
|---|---|---|
ntp.aliyun.com |
China | Alibaba Cloud NTP node, recommended for China, fast |
pool.ntp.org |
Global | Global public NTP, may be blocked or slow |
time.windows.com |
Global | Microsoft NTP server |
ntp.tencent.com |
China | Tencent NTP node, China alternative |
ESP32 uses standard C library time functions:
| Function | Description |
|---|---|
configTime(gmtOffset, dstOffset, server1, server2) |
Configure NTP server (ESP32 specific) |
setenv("TZ", tz_string, 1) |
Set timezone environment variable |
tzset() |
Apply timezone setting (must be called after setenv) |
time(&t) |
Get current time (seconds since Jan 1, 1970) |
localtime_r(&t, &tm) |
Convert seconds to readable time structure |
Key fields of tm structure:
| Field | Meaning | Range |
|---|---|---|
tm_year |
Year (from 1900; 2024 has value 124) | 0+ |
tm_mon |
Month (0 = January) | 0-11 |
tm_mday |
Day | 1-31 |
tm_hour |
Hour | 0-23 |
tm_min |
Minute | 0-59 |
tm_sec |
Second | 0-59 |
tm_wday |
Day of week (0 = Sunday) | 0-6 |
Beginner pitfall:
tm_yearneeds +1900 for actual year (2024 = tm_year 124),tm_monneeds +1 for actual month (January = tm_mon 0).
This tutorial uses ESP32's built-in WiFi.h and time.h, no third-party libraries needed.
If you haven't installed ESP32 board support yet, please refer to "Library Installation Guide" in Part 1 "WiFi Connection Tutorial".
configTime() — Configure NTP ServerconfigTime(gmtOffset_sec, daylightOffset_sec, server1, server2);
| Parameter | Description |
|---|---|
gmtOffset_sec |
UTC offset in seconds. Set to 0 when using TZ string |
daylightOffset_sec |
Daylight saving offset in seconds. Set to 0 |
server1 |
Primary NTP server address (string) |
server2 |
Backup NTP server address (optional) |
Example:
configTime(0, 0, "ntp.aliyun.com", "pool.ntp.org");
Note: Set
gmtOffset_secto 0 here because timezone is controlled by TZ string, not inconfigTime.
setenv() + tzset() — Set Timezonesetenv("TZ", "CST-8", 1); // Third parameter 1 means overwrite existing
tzset(); // Must be called to take effect
setenv("TZ", "CST-8", 1): Sets timezone environment variable to China Standard Timetzset(): Tells system "re-read timezone setting"Order matters: Must
setenvfirst, thentzset, and afterconfigTime.
time() — Get Current Timestamptime_t now;
time(&now); // Store current timestamp in now
Returns seconds from Jan 1, 1970 00:00:00 (UTC) to now. This value isn't intuitive by itself, needs localtime_r to convert.
localtime_r() — Convert to Readable Timestruct tm timeinfo;
localtime_r(&now, &timeinfo);
// timeinfo.tm_year = 124 (actual year 2024)
// timeinfo.tm_mon = 7 (actual month August)
// timeinfo.tm_mday = 10 (10th)
// timeinfo.tm_hour = 14 (14:00)
// timeinfo.tm_min = 30 (30 minutes)
// timeinfo.tm_sec = 45 (45 seconds)
// timeinfo.tm_wday = 6 (Saturday)
Why
localtime_rinstead oflocaltime?localtimereturns pointer to static memory, unsafe (multiple calls overwrite).localtime_rwrites to your provided structure, safer.
Reading Guide: Code is divided into 4 modules by function. Each module handles one independent category. Recommended to read in order.
Responsibility: Centralizes all adjustable parameters including WiFi credentials, NTP server, timezone, etc.
// --- WiFi Credentials ---
const char* WIFI_SSID = "YourWiFi";
const char* WIFI_PASS = "YourPassword";
// --- NTP Server ---
const char* NTP_SERVER_1 = "ntp.aliyun.com";
const char* NTP_SERVER_2 = "pool.ntp.org";
// --- Timezone ---
const char* TZ_INFO = "CST-8";
// --- Pin & Timing ---
#define LED_BUILTIN 2
const unsigned long WIFI_TIMEOUT_MS = 15000;
const unsigned long NTP_SYNC_TIMEOUT = 10000;
const unsigned long DISPLAY_INTERVAL = 1000;
Code Explanation:
| Parameter | Meaning | Adjustment Suggestion |
|---|---|---|
WIFI_SSID / WIFI_PASS |
WiFi credentials | Change to yours |
NTP_SERVER_1 |
Primary NTP server | ntp.aliyun.com recommended for China |
NTP_SERVER_2 |
Backup NTP server | Can set to empty string "" for single server |
TZ_INFO |
Timezone string | Modify based on your location |
WIFI_TIMEOUT_MS |
WiFi connect timeout (15 sec) | Increase for weak signal |
NTP_SYNC_TIMEOUT |
NTP sync timeout (10 sec) | Increase for slow network |
DISPLAY_INTERVAL |
Time display interval (1 sec) | Can change to 5 sec if too frequent |
Must modify: Change
WIFI_SSIDandWIFI_PASSto your WiFi info. If in China, recommend settingNTP_SERVER_1tontp.aliyun.com.
Responsibility: Connect to WiFi router and wait for success. This is prerequisite for NTP sync — network must exist first.
bool connectWiFi() {
Serial.print("[WiFi] Connecting to ");
Serial.println(WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_TIMEOUT_MS) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WiFi] Connected!");
Serial.print("[WiFi] IP: ");
Serial.println(WiFi.localIP());
Serial.print("[WiFi] RSSI: ");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
return true;
} else {
Serial.println("\n[WiFi] Failed to connect.");
return false;
}
}
Logic:
WiFi.mode(WIFI_STA): Switch to STA modeWiFi.begin(...): Initiate WiFi connectionWiFi.status() waiting for connection, check every 500ms, max wait 15 secondstruefalseDifference from Part 1: Uses non-blocking
whileloop instead of event callbacks. Simpler code, suitable for beginners. If WiFi can't connect, program stops in infinite loop at end ofsetup()(LED fast blink), prompting user to check WiFi info.
Responsibility: Configure NTP server and timezone, poll waiting for time sync to succeed.
bool syncNTP() {
// Step 1: Configure NTP server
configTime(0, 0, NTP_SERVER_1, NTP_SERVER_2);
// Step 2: Set timezone
setenv("TZ", TZ_INFO, 1);
tzset();
// Step 3: Wait for sync
Serial.println("[NTP] Waiting for time synchronization...");
unsigned long start = millis();
time_t now;
struct tm timeinfo;
while (millis() - start < NTP_SYNC_TIMEOUT) {
time(&now);
localtime_r(&now, &timeinfo);
// Valid synced time has year >= 2024 (tm_year >= 124)
if (timeinfo.tm_year >= 124) {
Serial.println("[NTP] Time synchronized!");
return true;
}
delay(200);
}
Serial.println("[NTP] Sync timeout.");
return false;
}
Three-step flow:
configTime(0, 0, server1, server2): Tells ESP32 which NTP servers to query. First two params set to 0 because timezone is controlled by TZ string.
setenv("TZ", "CST-8", 1) + tzset(): Sets timezone. setenv sets environment variable, tzset makes it effective. Must call in this order, and after configTime.
Poll for sync: Repeatedly call time() + localtime_r() to check if time is valid. Before sync, tm_year may be 70 (1970) or 124 (Jan 1, 2024); after sync it becomes actual year (e.g., 126 for 2026).
Key check: Use
tm_year >= 124(i.e., year 2024 or later) as "time synced" criterion. If sync fails, time stays at old value.
Beginner pitfall: NTP sync is asynchronous! After calling
configTime(), ESP32 sends request in background and waits for response.time()return value won't immediately become correct — must poll and wait.
Responsibility: Get current time, format to readable string, print to serial.
void printTime() {
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
// Format: YYYY-MM-DD HH:MM:SS (Weekday) TZ: CST-8
Serial.printf("[Time] %04d-%02d-%02d %02d:%02d:%02d",
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec);
// Weekday
const char* weekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
Serial.printf(" (%s)", weekdays[timeinfo.tm_wday]);
// Timezone
Serial.printf(" TZ: %s", TZ_INFO);
Serial.println();
}
Format Explanation:
Serial.printf uses printf-style format placeholders:
| Placeholder | Meaning | Example |
|---|---|---|
%04d |
4-digit integer, zero-padded | 2026 |
%02d |
2-digit integer, zero-padded | 08, 10 |
%s |
String | "Sat" |
Field Corrections:
tm_year + 1900: Year needs +1900 (because tm_year counts from 1900)tm_mon + 1: Month needs +1 (because tm_mon starts from 0)tm_wday: Use directly (0=Sunday, 6=Saturday)Output Example:
[Time] 2026-08-10 14:30:45 (Sat) TZ: CST-8
Responsibility: Call modules in order, drive the entire flow.
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================");
Serial.println(" ESP32 NTP Time Sync Example");
Serial.println("========================================\n");
pinMode(LED_BUILTIN, OUTPUT);
// Step 1: Connect to WiFi (Module 2)
if (!connectWiFi()) {
Serial.println("[ERROR] Cannot continue without WiFi.");
while (true) {
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
delay(200);
}
}
// Step 2: Sync NTP time (Module 3)
if (!syncNTP()) {
Serial.println("[WARN] Time sync may have failed. Check NTP server.");
}
// Step 3: Display time every second (Module 4)
Serial.println("\n[Time] Starting periodic time display...\n");
digitalWrite(LED_BUILTIN, HIGH);
unsigned long lastDisplay = 0;
while (true) {
unsigned long now = millis();
if (now - lastDisplay >= DISPLAY_INTERVAL) {
lastDisplay = now;
printTime();
}
delay(10);
}
}
void loop() {
// Never reached
}
Flow:
while(true) loop to continuously call printTime()Why
while(true)insetup()instead ofloop()? This tutorial only demonstrates "get time and print", no other tasks needed. Putting it insetup()is more intuitive. To add other features (like sensor control), move time display logic toloop().
const char* WIFI_SSID = "YourWiFiName";
const char* WIFI_PASS = "YourWiFiPassword";
If outside China, also modify:
const char* NTP_SERVER_1 = "pool.ntp.org"; // Change to nearby NTP server
const char* TZ_INFO = "EST+5"; // Change to your timezone
Normally, you should see:
========================================
ESP32 NTP Time Sync Example
========================================
[WiFi] Connecting to MyHomeWiFi
.....
[WiFi] Connected!
[WiFi] IP: 192.168.1.105
[WiFi] RSSI: -48 dBm
[NTP] Waiting for time synchronization...
[NTP] Time synchronized!
[Time] Starting periodic time display...
[Time] 2026-08-10 14:30:45 (Sat) TZ: CST-8
[Time] 2026-08-10 14:30:46 (Sat) TZ: CST-8
[Time] 2026-08-10 14:30:47 (Sat) TZ: CST-8
...
If WiFi connection fails:
[WiFi] Connecting to WrongWiFi
...................
[WiFi] Failed to connect.
[ERROR] Cannot continue without WiFi.
LED will fast blink, check if WiFi credentials are correct.
[WiFi] Connecting to MyHomeWiFi
.....
[WiFi] Connected!
[WiFi] IP: 192.168.1.105
[WiFi] RSSI: -45 dBm
[NTP] Waiting for time synchronization...
[NTP] Time synchronized!
[Time] Starting periodic time display...
[Time] 2026-08-10 14:30:00 (Sat) TZ: CST-8
[Time] 2026-08-10 14:30:01 (Sat) TZ: CST-8
[Time] 2026-08-10 14:30:02 (Sat) TZ: CST-8
[WiFi] Connecting to WrongWiFi
...................
[WiFi] Failed to connect.
[ERROR] Cannot continue without WiFi.
(LED flashes rapidly)
[WiFi] Connected!
[WiFi] IP: 192.168.1.105
[NTP] Waiting for time synchronization...
[NTP] Sync timeout. Using whatever time is available.
[WARN] Time sync may have failed. Check NTP server.
[Time] Starting periodic time display...
[Time] 1970-01-01 08:00:00 (Thu) TZ: CST-8
[Time] 1970-01-01 08:00:01 (Thu) TZ: CST-8
Scenario 3 means NTP server unreachable, time shows UTC+8 of 1970 (Jan 1, 1970 00:00:00 UTC = Beijing time 08:00:00). Check if NTP server address is correct.
Q1: Why does time show 1970 or 2024?
A: NTP sync failed. ESP32 starts time from default value (usually 1970 or Jan 1, 2024) on boot. Check:
Q2: Time is correct but timezone is wrong?
A: Check if TZ_INFO is set correctly. For example:
CST-8JST-9EST+5Q3: Why use ntp.aliyun.com instead of pool.ntp.org?
A: pool.ntp.org often times out or responds slowly in China. Alibaba's NTP server has nodes in China, faster and more stable. If outside China, pool.ntp.org works fine.
Q4: What does tm_year >= 124 mean in code?
A: tm_year counts years from 1900. 124 represents 1900 + 124 = 2024. Before NTP sync, time may stay at boot default (2024 or earlier); after sync it becomes current year (in 2026, tm_year = 126). So >= 124 checks if time is valid.
Q5: Does ESP32 need to re-sync after power off/reboot?
A: Yes. ESP32 has no clock battery, time is lost every power off. Must repeat "connect WiFi → sync NTP" flow each time.
Q6: How long does sync take?
A: Usually 1-5 seconds. May take longer if network is slow or NTP server responds slowly. Code sets 10-second timeout, modifiable via NTP_SYNC_TIMEOUT in Module 1.
Q7: Can I use multiple NTP servers simultaneously?
A: Yes. configTime supports two servers; first is primary, second is backup. If primary is unreachable, ESP32 automatically tries backup.
Q8: How to handle daylight saving time?
A: China doesn't use DST, so CST-8 is sufficient. In DST regions (like US, Europe), use TZ string with DST, such as PST+8PDT,M3.2.0,M11.1.0.
| Problem | Possible Cause | Solution |
|---|---|---|
| WiFi won't connect | Wrong credentials | Check case, reconfigure |
| WiFi won't connect | Weak signal | Move closer to router |
| NTP sync timeout | NTP server unreachable | Switch to ntp.aliyun.com or check firewall |
| Time shows 1970 | NTP not synced | Check NTP server address |
| Wrong timezone | TZ_INFO misconfigured | Modify TZ_INFO |
| Time jumps | NTP correction | Normal, corrects on sync |
| LED keeps blinking | WiFi connection failed | Check credentials |
If you have an OLED screen, display time simultaneously:
// After printTime(), add:
display.setCursor(0, 0);
display.printf("%04d-%02d-%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
display.setCursor(0, 16);
display.printf("%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
display.display();
if (timeinfo.tm_min == 0 && timeinfo.tm_sec == 0) {
Serial.println("=== Hour Chime ===");
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
}
if (timeinfo.tm_hour == 7 && timeinfo.tm_min == 0 && timeinfo.tm_sec == 0) {
Serial.println("Good morning!");
// Turn on lights, etc.
}
Add time before all Serial.print:
void logWithTimestamp(const char* msg) {
time_t now;
struct tm ti;
time(&now);
localtime_r(&now, &ti);
Serial.printf("[%02d:%02d:%02d] %s\n", ti.tm_hour, ti.tm_min, ti.tm_sec, msg);
}
void setTimezone(const char* tz) {
setenv("TZ", tz, 1);
tzset();
Serial.printf("[Timezone] Set to: %s\n", tz);
}
NTP uses UDP protocol port 123. If your router or firewall blocks UDP 123, ESP32 cannot sync time.
When NTP syncs, ESP32 may suddenly "jump" time to correct value (e.g., from 1970 to 2026). If your program has special operations during time jumps (like timers), handle this case.
tm_year and tm_mon Correctionstm_year must + 1900 for actual yeartm_mon must + 1 for actual monthtm_wday needs no correction (0=Sunday, 6=Saturday)ESP32 has no persistent clock, every power-on must repeat WiFi → NTP flow. If your application is sensitive to boot time, add "last sync time" check to skip if recently synced.
CST-8 means UTC+8 (East 8). Sign direction is easy to get wrong, remember the rule: TZ string offset sign is opposite of "UTC plus how much":
-8+5configTime() and setenv() OrderMust configTime() first, then setenv() + tzset(). If reversed, timezone setting may not take effect.
If your program gets time in multiple tasks/interrupts, use localtime_r (reentrant) not localtime (not reentrant).