Have you ever wanted to build a project that not only senses the world around it—but remembers it, too? With just a few components and a bit of code, you can teach your ESP32 to do exactly that. In this tutorial, you’ll take your first steps into data logging.
Project Overview
In this project, we’re going to build a data logger using the ESP32.

The ESP32 will constantly check the temperature and humidity every 2 seconds using a DHT22 sensor. For each reading, the ESP32 will get the current date and time from an NTP (Network Time Protocol) server, so every data point includes an accurate timestamp. The timestamp will be formatted as “YYYY-MM-DD HH:MM:SS”, which is easy for people to read and understand.
All the data will be saved on a microSD card in a file called “log.txt”. Each new measurement will be added to the file as a new line, showing the timestamp, temperature in degrees Celsius, and humidity as a percentage, all separated by commas.
Suggested Reading
Before you move forward with this project, it’s a good idea to check out the following tutorials. They’ll help you understand the basics of how to use a microSD card with ESP32 and how to get time from the internet using an NTP server.
Preparing the microSD card
Before using your microSD card in a project, it’s important to make sure it’s properly formatted with the correct file system—FAT16 or FAT32. This helps the ESP32 read and write files without any issues.
If you’re using a brand new SD card, it probably came already formatted with a FAT file system. However, this factory formatting might not be perfect, and you could run into problems. If you’re using an older card that’s been used before, it will definitely need to be reformatted. Either way, it’s a good idea to format the card before using it in your project.
There are two methods to format your microSD card:
Method 1
First, insert your microSD card into your computer. Then, find your SD card’s drive, and right-click on it. Choose Format from the menu. A window will pop up—select FAT32 as the file system, then click Start to begin formatting. Follow the instructions on the screen until it’s done.

Method 2
For better results and fewer errors, it’s highly recommended to use the official SD card formatter utility made by the SD Association. This tool is more reliable than the basic formatter that comes with your computer. You can download it from the SD Association’s website. Once installed, run the program, choose your SD card from the list of drives, and click the Format button. This special tool helps avoid common problems caused by bad or incomplete formatting, which can save you a lot of troubleshooting later on.

Wiring DHT22 Sensor and microSD Card Module to the ESP32
Now it’s time to connect the DHT22 sensor and the microSD card module to the ESP32.
For the microSD card module:
Start by connecting the 3V3 pin on the microSD card module to the 3.3V power pin on the ESP32. Then, connect the GND pin on the module to one of the GND pins on the ESP32.
Next, we’ll set up the pins used for SPI communication. Since microSD cards need fast data transfer, they work best when connected to the hardware SPI pins of the ESP32. On the ESP32, the default SPI pins are GPIO 18 for CLK, GPIO 19 for MISO, GPIO 23 for MOSI, and GPIO 5 for CS.
Here’s a quick reference table for the pin connections:
| microSD Card Module | ESP32 | |
| 3V3 | 3.3V | |
| CS | 5 | |
| MOSI | 23 | |
| CLK | 18 | |
| MISO | 19 | |
| GND | GND |
For the DHT22 sensor:
Connect the VCC pin on the DHT22 sensor to the 3.3V pin on the ESP32 and ground to ground. Then, connect the sensor’s Data pin to the ESP32’s D4 pin. To make sure the data signal is strong and clear, you need to add a 10K pull-up resistor between the Data pin and the VCC pin. However, if you are using a breakout board, you can skip adding this resistor because it’s already built into the board.
Here’s a quick reference table for the pin connections:
| DHTxx | ESP32 | Notes | |
| VCC | 3.3V | – | |
| GND | GND | – | |
| Data | D4 | pulled up by 10kΩ |
Complete Wiring
The diagram below shows exactly how to connect everything:

Setting Up the Arduino IDE
We will be using the Arduino IDE to program the ESP32, so please ensure you have the ESP32 add-on installed before you proceed:
Library Installation
The DHTxx sensors use a unique single-wire communication protocol to send temperature and humidity data. While this protocol is not a standard one, it is similar to the Dallas 1-Wire protocol and relies heavily on very precise timing to work correctly.
Luckily, you don’t need to worry about writing all the low-level code to handle this timing yourself. There’s a helpful library called the DHT sensor library that takes care of almost everything for you.
To install the library:
- First open your Arduino IDE program. Then click on the Library Manager icon on the left sidebar.
- Type “DHT sensor” in the search box to filter your results.
- Look for the “DHT sensor library” library created by Adafruit.
- Click the Install button to add it to your Arduino IDE.

Since the DHT sensor library relies on other libraries to function, you will be prompted to install its dependencies, which include the Adafruit Unified Sensor Library.
When this message appears, simply click INSTALL ALL to ensure everything is set up correctly.

Example Code
Copy the code below to your Arduino IDE. But before you upload the code to your ESP32, there are a few important changes you need to make to ensure everything works correctly for your setup.
- First, update the following two variables with your network credentials so that the ESP32 can connect to your network.
// Replace with your network credentials const char* ssid = "REPLACE_WITH_YOUR_SSID"; const char* password = "REPLACE_WITH_YOUR_PASSWORD"; - Next, you need to set the correct UTC offset for your time zone. UTC (Coordinated Universal Time) is a standard time used around the world, and your time zone is a certain number of hours ahead of or behind UTC. Refer to the list of UTC time offsets.
The offset must be written in seconds. For example, if you are in a time zone that is 5 hours behind UTC (like Eastern Standard Time in the U.S.), your offset would be -5 * 60 * 60, which equals -18000. If you are in Central European Time, which is UTC +1, your offset would be 1 * 60 * 60, or 3600. Here are a few examples:
- For UTC -5.00 : -5 * 60 * 60 : -18000
- For UTC +1.00 : 1 * 60 * 60 : 3600
- For UTC +0.00 : 0 * 60 * 60 : 0
const long gmtOffset_sec = -18000; - You also need to set the daylight saving time offset (in seconds). If your country or region observes daylight saving time, then set the daylight offset to 3600 seconds (which equals 1 hour). Otherwise, just set it to 0.
const int daylightOffset_sec = 3600;
After making these changes, go ahead and upload the code.
// Libraries for SD card
#include "FS.h"
#include "SD.h"
#include <SPI.h>
// Library for DHTxx sensor
#include "DHT.h"
// Libraries to get time from NTP Server
#include <WiFi.h>
#include "time.h"
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 2000; // Log data every 2 seconds
// DHT sensor setup
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE); // Initialize DHT sensor
// NTP server setup
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -18000;
const int daylightOffset_sec = 3600;
// Function that gets formatted date and time in YYYY-MM-DD HH:MM:SS format
String getFormattedDateTime() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return "";
}
char timeStringBuff[20]; // Buffer for formatted time string
strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(timeStringBuff);
}
// Function that writes to the SD card
void writeFile(fs::FS& fs, const char* path, const char* message) {
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing");
return;
}
if (file.print(message)) {
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
// Function that appends data to the SD card
void appendFile(fs::FS& fs, const char* path, const char* message) {
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if (!file) {
Serial.println("Failed to open file for appending");
return;
}
if (file.print(message)) {
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}
void setup() {
Serial.begin(115200);
// Initialize DHT sensor
dht.begin();
Serial.println("DHT sensor initialized");
// Connect to WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println("");
Serial.print("Connected to WiFi. IP address: ");
Serial.println(WiFi.localIP());
// Initialize SD card
if (!SD.begin()) {
Serial.println("Card Mount Failed");
return;
}
Serial.println("SD card initialized");
// Configure time with NTP server
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("NTP time configured");
// Check if the log file exists, create if it doesn't
File file = SD.open("/log.txt");
if (!file) {
Serial.println("Log file doesn't exist");
Serial.println("Creating log file...");
writeFile(SD, "/log.txt", "Timestamp,Temperature (°C),Humidity (%)\r\n");
} else {
Serial.println("Log file already exists");
}
file.close();
Serial.println("Setup complete. Starting data logging...");
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
// Get formatted date and time
String formattedDateTime = getFormattedDateTime();
// Get temperature and humidity readings
float temp = dht.readTemperature();
float hum = dht.readHumidity();
// Only proceed if we got valid data
if (formattedDateTime != "" && !isnan(temp) && !isnan(hum)) {
// Concatenate all info separated by commas
String dataMessage = formattedDateTime + "," + String(temp, 2) + "," + String(hum, 2) + "\r\n";
Serial.print("Saving data: ");
Serial.print("Time: ");
Serial.print(formattedDateTime);
Serial.print(", Temperature: ");
Serial.print(temp, 2);
Serial.print("°C");
Serial.print(", Humidity: ");
Serial.print(hum, 2);
Serial.println("%");
// Append the data to file
appendFile(SD, "/log.txt", dataMessage.c_str());
} else {
// Print error messages for debugging
if (formattedDateTime == "") {
Serial.println("Error: Could not obtain time from NTP server");
}
if (isnan(temp) || isnan(hum)) {
Serial.println("Error: Could not read from DHT sensor");
}
}
lastTime = millis();
}
}Demonstration
After uploading the sketch, open the Serial Monitor and make sure the baud rate is set to 115200. Then press the EN (reset) button on your ESP32. If everything is set up correctly, you should start seeing messages printed on the Serial Monitor showing the ESP32 connecting to Wi-Fi, getting the time from the internet, reading sensor data, and saving it to the SD card.

Let your ESP32 run for a while—maybe a few hours—so it can collect enough temperature and humidity data. When you’re ready to check the results, remove the microSD card from the module and insert it into your computer using an SD card reader. On the card, you should find a file called log.txt.

This file contains all the readings, including the date and time, temperature in degrees Celsius, and humidity in percentage.

You can open the log.txt file using a simple text editor, or better yet, copy its contents into Microsoft Excel or Google Sheets. Once there, you can create charts or graphs to help you visualize how the temperature and humidity changed over time. This makes it much easier to analyze the data.

Code Explanation
The sketch starts by including several libraries: The FS, SD, and SPI libraries allow the ESP32 to use the SPI protocol to talk to the microSD card and to handle file operations like creating, writing to, and reading from files. The DHT library helps the ESP32 to read temperature and humidity from the DHT22 sensor. The WiFi and time libraries let the ESP32 connect to the internet and get the current time from an NTP server.
// Libraries for SD card
#include "FS.h"
#include "SD.h"
#include <SPI.h>
// Library for DHTxx sensor
#include "DHT.h"
// Libraries to get time from NTP Server
#include <WiFi.h>
#include "time.h"Next, we define two variables for your Wi-Fi connection: one for the network name (SSID) and one for the password. This step is very important because the ESP32 needs to connect to your Wi-Fi in order to contact the NTP server and get the correct time. You have to replace the placeholder text with your actual network name and password.
// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";Next we create a simple timer using two variables. The lastTime variable remembers when we last logged data, and the timerDelay sets how often we log again. Here the timerDelay is set to 2000 milliseconds (2 seconds), so the ESP32 will try to log data every two seconds.
// Timer variables
unsigned long lastTime = 0;
unsigned long timerDelay = 2000; // Log data every 2 secondsNext, we tell the ESP32 which pin the DHT22 sensor is connected to. In our case, it’s GPIO pin 4. We also specify the type of DHT sensor we’re using. After that, we create a DHT object in the code. This object lets us use simple commands to get temperature and humidity readings from the sensor.
// DHT sensor setup
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE); // Initialize DHT sensorAfter that we configure the NTP settings. We choose a time server—in this case, “pool.ntp.org”—and tell the ESP32 how to adjust from UTC (Coordinated Universal Time) to your local time zone. We use gmtOffset_sec to shift the time zone and daylightOffset_sec to add daylight saving time if your location uses it. For example, a gmtOffset_sec of -18000 means UTC-5, which is used in Eastern Standard Time, and a daylight offset of 3600 adds one hour for daylight saving. You have to change these values based on where you live. If they aren’t correct, your timestamps will look wrong even though the NTP time is working fine.
// NTP server setup
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -18000;
const int daylightOffset_sec = 3600;The “pool.ntp.org” server is a good default because it automatically connects to a nearby time server. If you prefer, you can choose a regional server like “europe.pool.ntp.org” or “asia.pool.ntp.org” depending on where you are.
| Area | HostName |
| Worldwide | pool.ntp.org |
| Asia | asia.pool.ntp.org |
| Europe | europe.pool.ntp.org |
| North America | north-america.pool.ntp.org |
| Oceania | oceania.pool.ntp.org |
| South America | south-america.pool.ntp.org |
We then define a custom function called getFormattedDateTime(). This function asks the system for the current time, which was set earlier using the NTP server. It then formats the date and time into a human-readable string like “2025-08-13 16:45:23” using a built-in function called strftime(). If the function fails to get the time, it prints a message and returns an empty string.
// Function that gets formatted date and time in YYYY-MM-DD HH:MM:SS format
String getFormattedDateTime() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return "";
}
char timeStringBuff[20]; // Buffer for formatted time string
strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(timeStringBuff);
}We also create two more custom functions for writing to the SD card. The first one, writeFile(), is used to create a new file; it’s used at the beginning to create the log file with a header line. The second one, appendFile(), is used to add new data to the end of an existing file—this is what we use over and over again during data logging. Both functions open the file, try to write data, and then close the file when they’re done. If something goes wrong, they print a message to the Serial Monitor.
// Function that writes to the SD card
void writeFile(fs::FS& fs, const char* path, const char* message) {
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing");
return;
}
if (file.print(message)) {
Serial.println("File written");
} else {
Serial.println("Write failed");
}
file.close();
}
// Function that appends data to the SD card
void appendFile(fs::FS& fs, const char* path, const char* message) {
Serial.printf("Appending to file: %s\n", path);
File file = fs.open(path, FILE_APPEND);
if (!file) {
Serial.println("Failed to open file for appending");
return;
}
if (file.print(message)) {
Serial.println("Message appended");
} else {
Serial.println("Append failed");
}
file.close();
}In the setup() section, we start serial communication so we can print messages to the Serial Monitor. We also initialize the DHT sensor with dht.begin().
Serial.begin(115200);
// Initialize DHT sensor
dht.begin();
Serial.println("DHT sensor initialized");Then we try to connect the ESP32 to the Wi-Fi network. We use the WiFi.mode() function to put the Wi-Fi radio in station mode and the WiFi.begin() function and give it your Wi-Fi network’s name and password.
// Connect to WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);While the ESP32 is trying to connect, we check its connection status using WiFi.status(). If it’s not connected yet, it waits a bit and tries again until it succeeds.
Serial.print("Connecting to WiFi ..");
while (WiFi.status() != WL_CONNECTED) {
Serial.print('.');
delay(1000);
}
Serial.println("");For reference, the WiFi.status() function can return the following statuses:
WL_CONNECTED: Connected to a Wi-Fi network.WL_NO_SHIELD: No Wi-Fi shield detected (not applicable to ESP32, but relevant for some Arduino boards).WL_IDLE_STATUS: Temporary status during the connection attempt after callingWiFi.begin().WL_NO_SSID_AVAIL: No networks found matching the specified SSID.WL_SCAN_COMPLETED: The network scan has completed.WL_CONNECT_FAILED: Failed to connect after all attempts.WL_CONNECTION_LOST: The connection to the network was lost.WL_DISCONNECTED: Not connected to any network.
Once the ESP32 is connected to your Wi-Fi, it prints out the IP address it received from your router using WiFi.localIP() so you know it’s online.
Serial.print("Connected to WiFi. IP address: ");
Serial.println(WiFi.localIP());Next, we attempt to initialize the SD card. If the card fails to initialize, it will print an error message and stop.
// Initialize SD card
if (!SD.begin()) {
Serial.println("Card Mount Failed");
return;
}
Serial.println("SD card initialized");We then call the configTime() function to sync the ESP32’s internal clock with the NTP server using the time zone offsets we set earlier. Once that’s done, the ESP32 knows the correct date and time and will keep track of it even if the Wi-Fi disconnects.
// Configure time with NTP server
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("NTP time configured");Before we start logging data, we check whether the file /log.txt already exists on the microSD card. If it doesn’t, we create it and write a header line that labels the columns: Timestamp, Temperature (°C), and Humidity (%). This makes it easier to read the file later in Excel or Google Sheets. If the file already exists, we leave it alone to protect any old data. We then close the file and print a message saying the setup is complete so you know the system is ready to start recording.
// Check if the log file exists, create if it doesn't
File file = SD.open("/log.txt");
if (!file) {
Serial.println("Log file doesn't exist");
Serial.println("Creating log file...");
writeFile(SD, "/log.txt", "Timestamp,Temperature (°C),Humidity (%)\r\n");
} else {
Serial.println("Log file already exists");
}
file.close();
Serial.println("Setup complete. Starting data logging...");In the loop() function, the program checks how much time has passed by comparing the current time from millis() with the lastTime value. If the difference is greater than our timerDelay, then it’s time to log new data.
We call the getFormattedDateTime() function to get the current time as a string, and we use the DHT object to read the temperature and humidity.
if ((millis() - lastTime) > timerDelay) {
// Get formatted date and time
String formattedDateTime = getFormattedDateTime();
// Get temperature and humidity readings
float temp = dht.readTemperature();
float hum = dht.readHumidity();Next we verify that all three readings are valid: the time string must not be empty, and neither temperature nor humidity can be NaN (Not a Number). If they are, we create a line of data by joining the time, temperature (rounded to two decimal places), and humidity (also rounded), with commas between them and a newline at the end. We print this line to the Serial Monitor so you can see the data live, and then we call appendFile() to save it to the SD card in the log.txt file.
If the timestamp is empty or the sensor readings are invalid (which would show up as “NaN”), we print error messages to help with debugging.
// Only proceed if we got valid data
if (formattedDateTime != "" && !isnan(temp) && !isnan(hum)) {
// Concatenate all info separated by commas
String dataMessage = formattedDateTime + "," + String(temp, 2) + "," + String(hum, 2) + "\r\n";
Serial.print("Saving data: ");
Serial.print("Time: ");
Serial.print(formattedDateTime);
Serial.print(", Temperature: ");
Serial.print(temp, 2);
Serial.print("°C");
Serial.print(", Humidity: ");
Serial.print(hum, 2);
Serial.println("%");
// Append the data to file
appendFile(SD, "/log.txt", dataMessage.c_str());
} else {
// Print error messages for debugging
if (formattedDateTime == "") {
Serial.println("Error: Could not obtain time from NTP server");
}
if (isnan(temp) || isnan(hum)) {
Serial.println("Error: Could not read from DHT sensor");
}
}Finally, we update lastTime to the current millis() value so that the next logging cycle will start about two seconds later.
lastTime = millis();Troubleshooting
Card Mount Failed
If you see the error message “Card Mount Failed” in the Serial Monitor, this usually means there’s a problem with the way the SD card module is connected to the ESP32. Double-check all your wiring to make sure each wire goes to the correct pin and that they are firmly connected. Also, your microSD card must be formatted using FAT16 or FAT32 file systems—other formats like exFAT will not work.
Some websites might tell you to power your microSD card module using 5V, but do not do that unless you are absolutely sure your module has a built-in voltage regulator and logic level shifter. Most microSD cards are designed to work at 3.3 volts, and giving them 5V directly could permanently damage the card.
Could not obtain time from NTP server
Another common issue is the “Error: Could not obtain time from NTP server” message. This means the ESP32 couldn’t get the current time from the internet. The most likely reason is that your ESP32 didn’t successfully connect to the internet. Make sure that your network has internet access.
Also, be aware that the NTP system uses UDP port 123, and on some networks—especially school, enterprise, or guest networks—this port might be blocked by a firewall. If that’s the case, try connecting the ESP32 to a different Wi-Fi network, like your home router or mobile hotpot.
You can also try different NTP server addresses, such as “time.nist.gov“. You can also provide multiple servers in your configTime() function so that if one server fails, the others can act as backups.




