Cloud, DevOps e IoT en español

ESP32-C3 se desconecta de AWS IoT con CLIENT_ERROR: falta de stack en loop()

18 de octubre de 2026 · Steven Carvajal · Tutoriales

Código de este artículo en GitHub →

El síntoma es desconcertante: el ESP32 se conecta al Wi-Fi, abre la conexión TLS con AWS IoT Core, se conecta por MQTT… y un segundo después AWS lo desconecta. En los registros de AWS el motivo es CLIENT_ERROR. No hay reinicio, no hay Guru Meditation, y el mismo firmware funciona perfecto en un ESP32-S3.

En mi caso la causa fue la memoria de pila (stack) de la tarea que ejecuta loop(). Si es tu primera vez con esta placa, tienes la guía del ESP32.

Qué significa CLIENT_ERROR

AWS IoT usa CLIENT_ERROR cuando el dispositivo hace algo que no respeta el protocolo: un paquete MQTT mal formado, un tópico inválido, un mensaje que no se puede interpretar. Si tu código no cambió y en otra placa funciona, ese "paquete mal formado" suele venir de memoria corrupta: el búfer que se envía tiene basura.

Para ver el motivo, suscríbete en el cliente de pruebas MQTT de la consola de AWS IoT a:

$aws/events/presence/disconnected/+

El campo disconnectReason lo dice.

Por qué se corrompe la memoria

En el núcleo de Arduino para ESP32, setup() y loop() se ejecutan dentro de una tarea de FreeRTOS con una pila de 8 KB por defecto. Todo lo que declaras como variable local dentro de esas funciones (y de las que llamas) vive ahí.

Una conexión a AWS IoT gasta mucha pila de golpe:

Si la pila se desborda, pisa memoria vecina. A veces el sistema lo detecta y reinicia (Stack canary watchpoint triggered (loopTask)), pero otras veces solo corrompe datos y sigue funcionando… mal.

¿Por qué en el S3 no pasaba? La carga era la misma; el C3 tiene menos margen de RAM y una arquitectura distinta (RISC-V de un núcleo frente a Xtensa de dos), y la disposición de la memoria cambia. No lo tomes como regla: si tu firmware hace mucho, mide en cualquier placa.

Cómo medir la pila que te queda

void loop() {
  static uint32_t ultimo = 0;
  if (millis() - ultimo > 5000) {
    ultimo = millis();
    // Mínimo de pila libre que ha tenido esta tarea desde que arrancó (en bytes)
    Serial.printf("Pila libre mínima: %u bytes\n", uxTaskGetStackHighWaterMark(NULL));
    Serial.printf("Heap libre: %u bytes\n", ESP.getFreeHeap());
  }
  // ... tu código
}

Si la "pila libre mínima" baja de unos cientos de bytes después de conectar a AWS, estás al límite.

Salida esperada del monitor serie con la pila libre antes y después del cambio

La solución: más pila para loop()

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <MQTT.h>
#include <ArduinoJson.h>

SET_LOOP_TASK_STACK_SIZE(16 * 1024);   // 16 KB para setup() y loop()

void setup() { /* ... */ }
void loop() { /* ... */ }

Dos detalles:

Con 16 KB, el mismo firmware dejó de desconectarse en el C3.

Otras formas de gastar menos pila

Ampliar la pila es lo rápido. Si además quieres margen:

¿ESP32-S3 o ESP32-C3 para esto?

El C3 sirve como dispositivo conectado a AWS si ajustas la pila, pero tiene menos margen para crecer. Para el dispositivo que hace de puente con la nube prefiero el S3; los nodos sencillos pueden ser C3. Lo comparo en ESP32-S3 vs ESP32-C3.

La conexión completa está en conectar un ESP32 a AWS IoT Core paso a paso, y este error es uno de los 13 errores reales de mi proyecto de domótica.

Preguntas frecuentes

¿Cuánta pila debo poner?

16 KB resolvió una carga de TLS + MQTT + JSON + servidor web. Mide con uxTaskGetStackHighWaterMark() y deja al menos 1-2 KB libres en el peor momento.

¿Gastar más pila reduce la RAM disponible?

Sí, esos bytes se reservan al arrancar. En un ESP32-C3 con unos 400 KB de RAM, 8 KB extra rara vez es un problema.

¿Funciona igual con PlatformIO?

Sí, si usas el framework Arduino. En ESP-IDF puro el tamaño se configura con CONFIG_ESP_MAIN_TASK_STACK_SIZE en menuconfig.

Sigue leyendo

Guía completa de esp32 y firmware: Qué es el ESP32 y por qué domina los proyectos IoT: guía para elegir y empezar