Why Your sdkconfig Defaults Stopped Working in ESP-IDF v6

You pulled v6, ran idf.py build, and the binary came out. No errors. Tests pass locally. Then staging flashes it and the CPU’s running at the wrong frequency, BLE won’t init, and your carefully tuned log level is back to INFO.
In v6, your sdkconfig.defaults file is read, mostly respected, and quietly ignored in the places that matter most.
This article is the triage path. If you’re staring at a project that built fine last week and now produces the wrong firmware, start here. The fix is usually 30 minutes once you know where to look.
What Actually Changed in v6’s Config System
v5 was forgiving. Unknown Kconfig symbols in your defaults file? Accepted silently. Symbol renamed between minor versions? The old name often still worked through aliases. Type changed from bool to choice? You’d get a warning buried in 400 lines of CMake output.
v6 tightened all of that. Three changes matter:
Stricter symbol validation. Unknown symbols in sdkconfig.defaults now emit a warning and get dropped. The build still succeeds, which is exactly why you don’t notice until something runtime-breaks.
Reorganized namespaces. A chunk of CONFIG_ESP32_* symbols moved to CONFIG_ESP_* (target-agnostic) or to component-specific prefixes. The old names don’t alias in most cases.
Clearer precedence rules. refresh-config is now the canonical way to reconcile a stale sdkconfig against updated defaults, and it’s stricter about what counts as a conflict.
Here’s the resolution order you’re actually working with now:
┌─────────────────────────────────────┐
│ Kconfig symbol resolution (v6) │
├─────────────────────────────────────┤
│ 1. Command-line -D overrides │
│ 2. sdkconfig (if present & valid) │
│ 3. sdkconfig.defaults.<target> │
│ 4. sdkconfig.defaults │
│ 5. Kconfig default values │
│ → Unknown symbols: WARN + DROP │
└─────────────────────────────────────┘The critical bit: if sdkconfig exists and contains a valid value for a symbol, defaults files don’t touch it. That’s how v5 worked too, but v6’s stricter validation means a stale sdkconfig committed to your repo is now a much bigger liability. It can carry forward values that reference dead symbols. It can also miss new required symbols entirely.
The Four Failure Modes You’ll Actually Hit
┌──────────────────────────────────────────┐
│ SYMPTOM QUICK MATCH │
├──────────────────────────────────────────┤
│ Build OK, runtime wrong → Mode 1 │
│ Kconfig parse error → Mode 2 │
│ Wrong target defaults → Mode 3 │
│ CI ≠ local build → Mode 4 │
└──────────────────────────────────────────┘Mode 1: Silently dropped symbols
Symptom: Build succeeds. Firmware behaves wrong at runtime (wrong clock, wrong log level, missing feature).
Cause: Your sdkconfig.defaults references a symbol that was renamed in v6. v6 warns and drops it. The Kconfig default wins.
Fix: Grep your build output:
idf.py reconfigure 2>&1 | grep -iE "unknown|ignored|not found"Look for lines like warning: unknown kconfig symbol 'CONFIG_ESP32_DEFAULT_CPU_FREQ_240'. Those are the ones silently losing.
Mode 2: Type mismatch errors
Symptom: idf.py reconfigure fails with a parse error or invalid value message.
Cause: A symbol that was bool in v5 is now part of a choice group, or vice versa. Your CONFIG_FOO=y no longer parses cleanly.
Fix: Open the current Kconfig file for the component, check the new type, and update your defaults:
- CONFIG_COMPILER_OPTIMIZATION_SIZE=y
- CONFIG_COMPILER_OPTIMIZATION_DEFAULT=
+ CONFIG_COMPILER_OPTIMIZATION_SIZE=y
Choice groups only want the selected member set. Don’t explicitly unset the others.
Mode 3: Target-specific defaults not applied
Symptom: You build for esp32s3 and the esp32 defaults leak in because v6 is stricter about the sdkconfig.defaults.<target> filename. Confirm the target with idf.py set-target esp32s3, match the filename exactly (sdkconfig.defaults.esp32s3), then rebuild clean.
Mode 4: CI drift
Symptom: Local builds are correct. CI produces a different binary. Or the other way around.
Cause: Either CI is caching a stale sdkconfig from a previous IDF version, or you’re caching sdkconfig in one environment and not the other.
Fix: Don’t cache sdkconfig across IDF version changes. Delete it in CI before each build and let the defaults regenerate it. Your cache key should include the IDF version string.
sdkconfig.defaults Not Working: The 5-Minute Triage Checklist
Run this in order. Stop when one step reveals the problem.
# 1. Back up and remove the generated config
mv sdkconfig sdkconfig.backup
# 2. Refresh against current Kconfig definitions
idf.py refresh-config
# 3. Reconfigure and capture warnings
idf.py reconfigure 2>&1 | tee reconfigure.log
grep -iE "unknown|ignored|deprecated|not found" reconfigure.log
# 4. Diff old vs new to see what shifted
diff sdkconfig.backup sdkconfig | less
# 5. Re-pin your defaults based on the diffWhat each step actually tells you:
Step 1 removes the stale cache. If your project builds correctly after regeneration, you had a zombie sdkconfig carrying forward values that defaults would have overridden.
Step 2 reconciles your defaults against v6’s Kconfig tree. Any symbol that no longer exists surfaces here.
Step 3 is the payoff. Every “unknown symbol” line is a setting you thought you controlled but didn’t.
Step 4 shows you what v6’s defaults are now giving you versus what your old sdkconfig had. This is where you find the silent behavior changes.
Step 5 means taking the symbols from step 3, finding their new names, and updating sdkconfig.defaults. The next section has the common renames.
Fixing sdkconfig.defaults for v6
These are the renames that bite most projects. Verify against your exact v6 point release before copy-pasting; Espressif continues to refine the symbol tree across v6.x.
v5 Symbol → v6 Symbol / Status
──────────────────────────────────────────────────────────────
CONFIG_ESP32_DEFAULT_CPU_FREQ_* → CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_*
CONFIG_ESP32_XTAL_FREQ → CONFIG_XTAL_FREQ (or per-target)
CONFIG_COMPILER_OPTIMIZATION_* → restructured as a choice group
CONFIG_BT_ENABLED → unchanged
CONFIG_ESP32_PANIC_* → CONFIG_ESP_SYSTEM_PANIC_*
CONFIG_ESP32_WIFI_* → CONFIG_ESP_WIFI_* (most symbols)Split into target-specific defaults if you build for more than one chip family and your defaults have CPU frequency, PSRAM, flash size, or partition table settings that differ between targets. One sdkconfig.defaults with a pile of conditionals is exactly what v6’s stricter validation punishes.
Structure that works in v6:
project/
├── sdkconfig.defaults # common to all targets
├── sdkconfig.defaults.esp32 # ESP32-specific
├── sdkconfig.defaults.esp32s3 # ESP32-S3-specific
└── sdkconfig.defaults.esp32c6 # ESP32-C6-specificAnd the recommendation that saves the most future pain: version-control sdkconfig.defaults and its target variants, not sdkconfig. Add sdkconfig and sdkconfig.old to .gitignore. Let every build regenerate from defaults. IDF upgrades stop being mystifying.
If you’re building BLE firmware for asset tracking or similar, audit the Kconfig symbols around BLE advertising intervals, TX power, and scan parameters carefully during migration. The Hubble SDK’s terrestrial device integration guide shows the advertising parameters you’d want to pin explicitly rather than rely on defaults.
CI Pipeline Updates
Three changes to your pipeline the day you bump to v6:
Remove
sdkconfigfrom any cache. Cache ccache and the IDF tools install, not the generated config. Include the IDF version in your cache key so a tools upgrade invalidates cleanly.Run
idf.py refresh-configas a pre-build step and fail the build on any “unknown symbol” or “ignored” warning. One-liner:idf.py refresh-config 2>&1 | tee config.log ! grep -qE "unknown|ignored" config.logPin the IDF version explicitly (tag or commit SHA). Tracking “latest v6” while the config system is still settling will bite you.
If you’re using the Hubble device SDK reference applications, the example CI configs there already follow this pattern; worth cross-referencing if you’re setting up a pipeline from scratch.
Making the Migration Stick
This is a one-time tax, not a recurring one. The v5-to-v6 config refactor is deliberately strict because v5’s silent fallbacks hid bugs for years. Once you’ve cleaned your sdkconfig.defaults, split by target where needed, and stopped committing sdkconfig itself, future IDF upgrades get a lot quieter.
If you’re mid-migration and stuck on a symbol that isn’t in the table above, check the v6 release notes for your exact point version and search the Espressif GitHub for the old symbol name in closed PRs. Someone renamed it, and the PR usually explains why.
Hubble Network lets ESP32 devices connect to satellites directly from Bluetooth, no gateways or cellular required. See how it works →