ESP32 Partition Table: Designing Flash Layout for Production Firmware
The partition table is one of those things that almost every tutorial glosses over. They use the default partition table, flash the demo, and move on. But when you are shipping production firmware to thousands of devices, your partition layout is a critical architectural decision that determines whether OTA updates work reliably, whether devices can recover from bad firmware, and whether you have enough space for firmware growth over the product lifetime.
Understanding the Partition Table Format
The ESP32 partition table is a binary structure stored at address 0x8000 in flash. It is generated from a CSV file during the build process. Each row defines one partition.
Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x10000, 1M,
ota_0, app, ota_0, 0x110000, 1M,
ota_1, app, ota_1, 0x210000, 1M,
coredump, data, coredump, 0x310000, 64K,
storage, data, spiffs, 0x320000, 960K,
This layout targets a 4 MB flash (the most common ESP32 module size). Let us walk through each partition.
Key Partitions Explained
nvs (Non-Volatile Storage): This is where ESP-IDF stores WiFi credentials, device configuration, and any key-value data your firmware writes. 24 KB (0x6000) is the minimum; allocate more if you store significant configuration data.
otadata: 8 KB control structure that tracks which OTA partition to boot from and whether an OTA update is pending or confirmed. Never reduce this below 8 KB.
phy_init: RF calibration data written at manufacturing. Do not remove this.
factory: The baseline firmware that is always available. If both OTA partitions are invalid, the bootloader falls back here. This is your recovery partition.
ota_0 / ota_1: The A/B update partitions. OTA updates write to whichever slot is not currently running, then the bootloader switches to the new slot on next boot.
coredump: When firmware crashes, the core dump is written here. Retrieve it over serial or upload to a crash analytics server.
storage (SPIFFS/LittleFS): For storing configuration files, certificates, HTML for captive portals, etc.
Sizing OTA Partitions
Your OTA partitions must be at least as large as your largest expected firmware binary — with room to grow. Common mistake: setting OTA partitions to 1 MB when the firmware is already 900 KB and you have six months of features to add.
Rule of thumb: OTA partition size = current firmware size × 2, rounded up to the nearest 64 KB boundary.
For most production ESP-IDF projects:
On a 4 MB flash with 1 MB OTA partitions: you use 1 MB (factory) + 1 MB (ota_0) + 1 MB (ota_1) = 3 MB for firmware, leaving 1 MB for NVS, otadata, phy, coredump, and storage. That is tight. Consider 8 MB flash modules (ESP32-WROVER) for firmware-heavy projects.
Reading and Writing NVS from C
NVS uses a namespace/key/value model. Always use namespaces to avoid key collisions between components.
#include "nvs_flash.h"
#include "nvs.h"#define NVS_NAMESPACE "device_cfg"
esp_err_t config_write_wifi_credentials(const char *ssid, const char *password) {
nvs_handle_t handle;
esp_err_t err;
// Open namespace in read-write mode
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) return err;
err = nvs_set_str(handle, "wifi_ssid", ssid);
if (err != ESP_OK) goto cleanup;
err = nvs_set_str(handle, "wifi_pass", password);
if (err != ESP_OK) goto cleanup;
// Commit changes — mandatory for writes to be persisted
err = nvs_commit(handle);
cleanup:
nvs_close(handle);
return err;
}
esp_err_t config_read_wifi_credentials(char *ssid, size_t ssid_len,
char *pass, size_t pass_len) {
nvs_handle_t handle;
esp_err_t err;
err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) return err;
err = nvs_get_str(handle, "wifi_ssid", ssid, &ssid_len);
if (err != ESP_OK) goto cleanup;
err = nvs_get_str(handle, "wifi_pass", pass, &pass_len);
cleanup:
nvs_close(handle);
return err;
}
Always handle ESP_ERR_NVS_NOT_FOUND: On first boot, keys do not exist. Your firmware must handle this gracefully and enter a provisioning mode.
OTA Update Flow in Code
#include "esp_ota_ops.h"
#include "esp_https_ota.h"void perform_ota_update(const char *firmware_url) {
esp_https_ota_config_t ota_config = {
.http_config = &(esp_http_client_config_t){
.url = firmware_url,
.cert_pem = server_root_ca, // Verify server cert
.timeout_ms = 10000,
.keep_alive_enable = true,
},
};
ESP_LOGI("OTA", "Starting OTA from: %s", firmware_url);
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI("OTA", "OTA succeeded — restarting");
esp_restart();
} else {
// OTA failed — current firmware still running, no damage done
ESP_LOGE("OTA", "OTA failed: %s", esp_err_to_name(ret));
}
}
After restart, the new firmware must call esp_ota_mark_app_valid_cancel_rollback() to confirm it boots successfully. If it does not (because it crashes), the bootloader automatically rolls back to the previous partition. This is your safety net.
Factory Reset Partition Strategy
Store a known-good firmware image in the factory partition at manufacturing time. In the field, if both OTA partitions become corrupted (power loss during two consecutive updates), the bootloader boots factory firmware. From there, the device re-downloads a fresh OTA image.
To trigger a factory reset from firmware:
void factory_reset(void) {
// Erase otadata — bootloader will boot factory partition next
const esp_partition_t *otadata = esp_partition_find_first(
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_OTA, "otadata"); if (otadata) {
esp_partition_erase_range(otadata, 0, otadata->size);
}
nvs_flash_erase(); // Clear all device configuration
esp_restart();
}
A well-designed partition table is the foundation of a reliable OTA update system. Invest time here before your first production build — changing it later requires a full reflash of every device in the field.
For deep-dive coverage of secure boot and flash encryption layered on top of this partition layout, see our guide on [IoT bootloader design and secure boot](/iot-bootloader-secure-boot).
[Contact Code Caracal](/contact) — we build production firmware for clients across 15+ countries.