ESP-IDF menuconfig Explained: Every Setting That Affects Power and BLE and Memory

Your ESP32 is pulling 28 mA in “deep sleep.” Your BLE stack just ate 60 KB of RAM you were counting on. Your firmware image won’t fit after you added one more library. All three problems live in the same place: a Kconfig menu you scrolled through once, felt overwhelmed by, and closed.
idf.py menuconfig has well over a thousand options by the time you count every component. Espressif ships defaults that make examples work on any dev board, which isn’t the same as shipping a battery-powered product. The documentation won’t tell you which knobs matter, so teams either accept the defaults (and ship bloated firmware) or copy a random sdkconfig from GitHub (and inherit someone else’s mistakes).
This is the guide I wish I’d had when I started. ESP32 original silicon, ESP-IDF v5.5+, and the 20-ish settings that actually move the needle on power, BLE, and memory. Opinionated recommendations included.
How menuconfig and sdkconfig Actually Work
Before touching anything, understand the pipeline:
menuconfig (UI)
│
▼
sdkconfig ──► sdkconfig.h ──► #ifdefs in your build
▲
│
sdkconfig.defaults (checked into git)Each IDF component ships a Kconfig file declaring its options. At build time, IDF assembles them into the menu you see. Your choices get written to sdkconfig (key=value text) and mirrored into sdkconfig.h (C macros). The build then compiles everything behind #ifdef CONFIG_WHATEVER accordingly.
The rule I give teams I’ve worked with: gitignore sdkconfig, commit sdkconfig.defaults. The first is a local scratchpad. The second is your team’s source of truth. Skip this and two engineers will spend a Friday afternoon chasing a “works on my machine” bug that turns out to be a flipped Kconfig flag.
Handy trick: after you’ve dialed things in via the menuconfig UI, run idf.py save-defconfig. It spits out a minimal diff against the board defaults, which is exactly what belongs in sdkconfig.defaults. Don’t paste a full sdkconfig in there. Future you will hate past you.
Power Settings That Actually Move the Needle
If you’re building anything battery-powered, these are the levers. I’ve seen devices drop from 25 mA to under 1 mA idle just by flipping the right three flags.
CPU frequency Component config → ESP System Settings → CPU frequency
Options: 80 / 160 / 240 MHz. Set to 80 MHz unless you have measured evidence you need more. Halving the clock roughly halves active-mode current. Most sensor firmware spends 99% of its time waiting.
Power management Component config → Power Management → Support for power management (CONFIG_PM_ENABLE)
Turn this ON. It enables Dynamic Frequency Scaling and lets the IDF drop the CPU clock when nothing’s happening. The single biggest miss I see in new projects.
Tickless idle Component config → FreeRTOS → Use tickless idle (CONFIG_FREERTOS_USE_TICKLESS_IDLE)
ON, with a tick idle threshold of 3. Without this, FreeRTOS wakes the core 100 times a second just to increment a counter. With it, the CPU actually stays asleep between real work.
RTC slow clock source Component config → Hardware Settings → RTC Clock Config
Use the external 32.768 kHz crystal if your hardware has one. The internal 150 kHz RC oscillator drifts badly with temperature, which wrecks BLE connection timing during long sleeps and produces sloppy wake intervals.
Automatic light sleep CONFIG_PM_SLP_DEFAULT_PARAMS and related. Pairs with PM_ENABLE. The CPU drops into light sleep whenever no task is ready. Enable it once your peripherals (I2C, SPI) are properly quiesced. Skip that step and you’ll chase weird hangs for an afternoon.
Brownout detector
Leave alone unless you’ve measured rail dips under TX bursts. If you’re seeing resets during BLE advertising on a small LDO, loosen it a step.
If your device draws 30 mA sitting idle, you almost certainly forgot
CONFIG_PM_ENABLE. It’s alwaysCONFIG_PM_ENABLE.
BLE Settings That Determine Whether Your Product Works
The ESP32 ships with two BLE host stacks. Picking the wrong one costs you 40 KB of RAM and a lot of headaches.
Host stack choice Component config → Bluetooth → Host
Use NimBLE. It’s smaller, the API is cleaner, and Apache actively develops it. Bluedroid is the legacy Android port. It works, but it’s heavier and the code is harder to reason about. Switching a recent project from Bluedroid to NimBLE reclaimed 38 KB of RAM. No functional loss.
BLE only vs Dual Mode Component config → Bluetooth → Bluetooth controller → Bluetooth controller mode
Set to BLE Only. BT Classic on ESP32 works but you almost certainly don’t need it, and disabling it saves roughly 40 KB of flash plus RAM for controller buffers.
Max connections CONFIG_BT_NIMBLE_MAX_CONNECTIONS
Defaults tend to be generous (often 3 or more). Every connection slot reserves RAM whether you use it or not. A sensor peripheral needs 1. A hub might need 3 or 4. Don’t leave it at 9 “just in case.”
MTU size CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU. Default is 256 in recent NimBLE versions, but verify. For sensor streaming, set it to 247. That’s the largest payload that fits in a single link-layer PDU when you’re running the 2M PHY. For occasional small notifications, leave it modest to save buffers.
Host and controller task stacks
If you see stack overflow panics in btu_task or nimble_host, bump these by 512 bytes and move on. Don’t preemptively inflate them.
BLE 5.0 features: extended advertising, coded PHY, 2M PHY
Each costs code size. Only enable what you use. 2M PHY is basically free wins if both ends support it. Coded PHY (long range) is a niche feature; don’t enable it by reflex.
NimBLE log level
Set to WARN in production builds. The INFO level spams the UART and, on a loaded BLE link, can actually cause disconnects because logging blocks the host task.
Memory Settings That Reclaim Kilobytes
Most ESP32 firmware leaks memory through defaults, not bugs. Here’s where the kilobytes hide.
Compiler optimization CONFIG_COMPILER_OPTIMIZATION
Change from Default (-Og) to Size (-Os) for release builds. Typical saving: 15 to 25 KB of flash, sometimes more. Keep -Og on dev branches so breakpoints behave.
Ever wonder why your release build is still dragging 40 KB of log format strings? Two settings control this. Log default level (CONFIG_LOG_DEFAULT_LEVEL) should be Info in development, Warn or Error in release. Log maximum level caps what can be re-enabled at runtime; set it equal to your default in production, since there’s no reason to carry strings you can’t print. Together they’re worth 5 to 10 KB on a logging-heavy codebase.
Main task stack size CONFIG_ESP_MAIN_TASK_STACK_SIZE
Default 3,584 bytes is fine. Don’t pad “just in case.” Use uxTaskGetStackHighWaterMark() to measure actual usage before touching it.
Newlib nano formatting CONFIG_NEWLIB_NANO_FORMAT
ON. Smaller printf, no float formatting by default. Saves 20 to 30 KB of flash. If you need %f in a few places, newlib_nano has workarounds, or enable full formatting only where you truly need it.
Heap poisoning CONFIG_HEAP_POISONING_*
Light in development (catches most corruption). None in release (saves both RAM and cycles on every allocation).
Wi-Fi static vs dynamic buffers
If you use Wi-Fi, switch RX/TX buffers to dynamic. Static buffers sit reserved even when idle. Dynamic lets the heap breathe when the radio is quiet.
PSRAM (if your module has it)
Enable it, and set allocation strategy thoughtfully. “Try internal first, fall back to PSRAM” is usually right, because internal RAM is faster and DMA-capable.
Memory wins by setting (typical ESP32 project):
Compiler -Os .............. ~15-25 KB flash
NimBLE vs Bluedroid ....... ~30-40 KB RAM
Log level WARN ............ ~5-10 KB flash
Newlib nano ............... ~20-30 KB flash
No BT Classic ............. ~40 KB flashIt’s not unusual to reclaim 80 to 120 KB of flash and 40 KB of RAM just from config hygiene.
An Opinionated sdkconfig.defaults Starting Point
Paste this into a new project, then adjust to fit your hardware and product requirements:
# Build size
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_NEWLIB_NANO_FORMAT=y
# Logging
CONFIG_LOG_DEFAULT_LEVEL_WARN=y
# CPU and power
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80=y
CONFIG_PM_ENABLE=y
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
# Bluetooth
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
# Clock
CONFIG_ESP32_XTAL_FREQ_40=yYou’ll add partition table changes, Wi-Fi settings, and component-specific flags as the project grows. Starting here puts you ahead of 80% of ESP32 projects I’ve seen reviewed.
What I Don’t Touch (and Why)
- Partition table: defaults work until you need OTA or bigger NVS. Change it deliberately, not out of boredom.
- Wi-Fi internals (AMPDU, buffer counts): the defaults are tuned by people who measure this for a living.
- FreeRTOS tick rate: 100 Hz is right for almost everyone. Raising it costs power, lowering it breaks timing assumptions in drivers.
- Interrupt allocation flags: drivers know what they need. Overriding usually causes pain.
Three Habits To Adopt Monday Morning
Treat sdkconfig as a design artifact. Review it like you’d review a schematic: deliberately, before every release, with someone else looking over your shoulder.
- Commit
sdkconfig.defaults, gitignoresdkconfig. Non-negotiable on teams. - Run
idf.py size-componentsbefore and after every config change. If you can’t tell what a flag did, revert it. - Re-audit your
sdkconfig.defaultsbefore every production build. Flags that made sense six months ago may not now.
Hubble Network provides global BLE connectivity for ESP32 devices without the firmware bloat of cellular or Wi-Fi stacks. See how it works →