The Short Answer: How to Increase ESP32 Main Stack Size

If your ESP32 is crashing during heavy operations like HTTPS requests or SD card logging, you are likely blowing the default stack limit. In the Arduino ESP32 Core (v2.x and v3.x), the main loopTask defaults to an 8192-byte (8KB) stack. In native ESP-IDF, app_main defaults to just 4096 bytes (4KB).

The definitive fix: Do not rely on the default loop() for heavy lifting. Instead, spawn a dedicated FreeRTOS task with a 16384-byte (16KB) or 32768-byte (32KB) stack using xTaskCreatePinnedToCore(), and leave the main loop() empty. If you are using native ESP-IDF or PlatformIO, change CONFIG_ESP_MAIN_TASK_STACK_SIZE in your sdkconfig file.

Decision Tree: Which Method Should You Use?
Your EnvironmentConstraintConcrete Pick / Action
Arduino IDE / Core v3.xNeed >8KB stack quicklyUse xTaskCreatePinnedToCore (Method 1 below)
PlatformIO / ESP-IDFNative C/C++ buildEdit sdkconfig CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
Any EnvironmentNeed >32KB stack (e.g., heavy JSON/LVGL)Allocate task stack in PSRAM using xTaskCreateStaticPinnedToCore

Anatomy of the Crash: Exact Error Strings and Ranked Causes

When the ESP32 writes past the allocated stack boundary, it overwrites a hidden guard value known as the "stack canary." The hardware watchdog catches this and triggers a core panic. If you see either of the following exact error strings in your serial monitor, you have a stack overflow:

Exact Error String 1:
Stack canary watchpoint triggered (loopTask)
Guru Meditation Error: Core 1 panic'ed (Unhandled debug exception)
Exact Error String 2 (Older Cores / ESP-IDF):
abort() was called at PC 0x4008xxxx on core 1
Stack smashing protect failure

Ranked Causes (Most to Least Likely):

  1. TLS/HTTPS Handshakes: The WiFiClientSecure library (mbedTLS) requires 6,000 to 9,000 bytes of stack space just to negotiate a certificate. If your base stack is 8KB, you will crash before the connection opens.
  2. Large Local Arrays: Declaring char buffer[4096]; inside a function called from loop() instantly consumes half your stack.
  3. FatFS / SD Card Operations: Mounting an SD card and writing via the FAT filesystem triggers deep recursive directory parsing that spikes stack usage.
  4. Deep Recursion: Libraries like ArduinoJson parsing deeply nested payloads, or LVGL UI rendering loops, will quietly walk off the edge of the stack.

Hardware Context: Why Your Sensor Build is Starving the Stack

To demonstrate the fix, we will use a notoriously stack-heavy build: an environmental logger that reads an I2C sensor, writes to an SPI SD card, and uploads via HTTPS. This specific combination is the most common trigger for stack canary failures on the bench.

Target Board Variant: ESP32-WROOM-32 DevKit v1 (Standard 4MB Flash, no PSRAM). Note: If using an ESP32-S3 or WROVER with PSRAM, see the extension section at the end.

Parts List & Pin Mapping

ComponentProtocolESP32 Pin (GPIO)Notes
ESP32-WROOM-32 DevKit v1--Main MCU
MicroSD Card Module (SPI)SPICS=5, MOSI=23, MISO=19, SCK=18Use 5V tolerant module with LDO
Adafruit BME280 SensorI2CSDA=21, SCL=22Address 0x77

Method 1: The FreeRTOS Task Offload (Recommended for Arduino Core)

Rather than fighting the Arduino IDE to change compile-time sdkconfig flags (which often get overwritten on core updates), the most robust engineering practice is to offload the heavy work to a custom FreeRTOS task. This leaves the 8KB loopTask completely untouched while giving your heavy lifting 16KB or more.

Below is the complete, compilable code. It includes explicit pin definitions, task creation error handling, and the high-water mark debugging tool.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <SD.h>
#include <Adafruit_BME280.h>

// --- Pin Definitions ---
#define SD_CS_PIN    5
#define SD_MOSI_PIN  23
#define SD_MISO_PIN  19
#define SD_SCK_PIN   18
#define I2C_SDA_PIN  21
#define I2C_SCL_PIN  22

// --- Task Parameters ---
#define HEAVY_TASK_STACK_SIZE 16384 // 16KB stack (Default loop is 8KB)
#define HEAVY_TASK_PRIORITY   2
#define HEAVY_TASK_CORE       1     // Pin to Core 1 (Core 0 handles WiFi)

Adafruit_BME280 bme;
WiFiClientSecure secureClient;
TaskHandle_t heavyTaskHandle = NULL;

// The heavy lifting function
void heavyLoggingTask(void * parameter) {
    secureClient.setInsecure(); // Bypass cert validation for this demo
    
    for(;;) {
        float temp = bme.readTemperature();
        
        // SD Card Write (Stack spike #1)
        File dataFile = SD.open("/log.csv", FILE_APPEND);
        if(dataFile) {
            dataFile.print(temp);
            dataFile.print(",");
            dataFile.println(bme.readHumidity());
            dataFile.close();
        }
        
        // HTTPS Request (Stack spike #2 - requires ~7KB stack alone)
        if(WiFi.status() == WL_CONNECTED) {
            if(secureClient.connect("api.example.com", 443)) {
                secureClient.println("GET / HTTP/1.1");
                secureClient.println("Host: api.example.com");
                secureClient.println("Connection: close");
                secureClient.println();
                while(secureClient.connected() && secureClient.available()) {
                    secureClient.read(); // Drain buffer
                }
                secureClient.stop();
            }
        }
        
        // Debug: Check how much stack is actually left
        UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(heavyTaskHandle);
        Serial.printf("Stack High Water Mark: %u bytes free\n", highWaterMark);
        
        vTaskDelay(pdMS_TO_TICKS(10000)); // Wait 10 seconds
    }
}

void setup() {
    Serial.begin(115200);
    
    // Initialize I2C with explicit pins
    Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
    if(!bme.begin(0x77)) {
        Serial.println("BME280 init failed. Halting.");
        while(1) delay(10);
    }
    
    // Initialize SPI and SD
    SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
    if(!SD.begin(SD_CS_PIN)) {
        Serial.println("SD Card init failed. Halting.");
        while(1) delay(10);
    }
    
    WiFi.begin("YOUR_SSID", "YOUR_PASS");
    while(WiFi.status() != WL_CONNECTED) {
        delay(500);
    }
    
    // Spawn the task with increased stack
    BaseType_t result = xTaskCreatePinnedToCore(
        heavyLoggingTask,
        "HeavyLogger",
        HEAVY_TASK_STACK_SIZE,
        NULL,
        HEAVY_TASK_PRIORITY,
        &heavyTaskHandle,
        HEAVY_TASK_CORE
    );
    
    if(result != pdPASS) {
        Serial.println("Failed to create heavy task! Insufficient heap.");
        while(1) delay(10);
    }
}

void loop() {
    // Leave entirely empty. The main loopTask retains its default 8KB stack
    // and does nothing, preventing any background Arduino core stack clashes.
    vTaskDelay(pdMS_TO_TICKS(1000));
}
Crucial Error Handling Note: Notice the if(result != pdPASS) check after xTaskCreatePinnedToCore. If the ESP32 lacks contiguous heap memory to allocate your requested 16KB stack, the function fails silently and returns errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY. Always check this return value, or your task simply won't run and you'll spend hours debugging a "frozen" sketch.

Method 2: Modifying sdkconfig (For ESP-IDF and Advanced Arduino)

If you are writing native ESP-IDF code, or using PlatformIO where you have direct access to the build flags, you can change the compile-time default for the main task.

  1. Open your sdkconfig file (or sdkconfig.defaults).
  2. Locate the line: CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096
  3. Change it to: CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
  4. Run idf.py fullclean and rebuild.

Warning for Arduino IDE users: While you can technically edit the sdkconfig hidden inside the Arduino15 packages folder, the Arduino IDE will overwrite this file every time you update the ESP32 board manager package. Stick to Method 1 for Arduino environments.

First Three Things to Check When It Still Fails

If you have increased the stack to 16KB and are still triggering the stack canary watchpoint, run through this diagnostic sequence:

  1. Verify You Are Measuring Stack, Not Heap: A common bench mistake is calling ESP.getFreeHeap() and assuming the stack is fine. Heap and stack are entirely different memory regions. You must use uxTaskGetStackHighWaterMark(NULL) inside the crashing task. If this returns a number under 500, you are still starved for stack space.
  2. Hunt Hidden Local Buffers: Search your code for arrays declared inside functions. A String object that concatenates large JSON payloads dynamically will allocate on the heap, but a char json_buf[8192]; declared at the top of a function allocates directly on the stack. Move large buffers to the global scope or allocate them dynamically via malloc() (which uses the heap).
  3. Check Library Stack Hogs: Some libraries are notoriously poorly written regarding stack usage. For example, older versions of the PubSubClient (MQTT) library hardcoded large local buffers for packet assembly. Update your libraries via the IDE manager, or check the library's source code for large local array declarations.

Extending and Simplifying Your Build

To Simplify (Reduce Stack Need): If you do not strictly need TLS encryption, drop WiFiClientSecure in favor of standard WiFiClient. A standard HTTP request uses less than 1KB of stack space compared to the 7KB+ required for mbedTLS handshakes. Similarly, format SD cards as FAT32 with 32KB cluster sizes to reduce FatFS directory parsing depth.

To Extend (Beyond 32KB Stack): The ESP32-WROOM-32 has limited internal SRAM (~520KB usable). If your application (like an LVGL display driver or heavy audio DSP) requires a 64KB+ stack, internal RAM will fragment and fail allocation. You must upgrade to an ESP32-WROVER or ESP32-S3 module featuring PSRAM. When using PSRAM, allocate the task stack statically in external RAM using xTaskCreateStaticPinnedToCore(), passing a buffer allocated via heap_caps_malloc(SIZE, MALLOC_CAP_SPIRAM). This offloads the massive stack requirement to the external chip, leaving internal SRAM free for fast DMA and WiFi buffers.

For further reading on FreeRTOS memory management and task creation parameters, refer to the official Espressif FreeRTOS API Documentation and the Arduino ESP32 Core Repository for core-specific memory mapping updates.