Articles / Embedded

  • esp8266
  • arduino
  • tls
  • heap
  • embedded
  • debugging

Why an ESP8266 Reboots on an HTTPS POST When Heap Runs Short

Plain HTTP POSTs work for days, then you switch to HTTPS and the board resets the moment it sends. No error, no message, just the boot banner again. Nine times out of ten the cause is memory: TLS wants a big contiguous chunk of heap, and an ESP8266 with WiFi running does not have much to give.

Read the reset reason before guessing

The chip tells you why it restarted, if you ask. Print it first thing in setup(), before anything else can fail.

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println(ESP.getResetInfo());
}

You will usually see one of two things. "Soft WDT reset" means something ran too long without yielding. "Exception (28)" or "Exception (29)" means a load or store through a bad pointer, and after a failed allocation that pointer is very often null.

Why TLS is the expensive part

A TLS record can be up to 16 KB, so the BearSSL client in the Arduino core allocates a receive buffer of roughly that size by default, plus a small send buffer. On a chip with perhaps 40 KB of free heap once WiFi is up, one connection takes a large share of everything you have.

The handshake also does certificate processing and public-key maths on top of the buffers. So the peak, not the steady state, is what kills you. A sketch that looks healthy at idle can fall over for the two seconds the handshake is running.

Free heap lies; the largest free block does not

This is the bit that catches people. ESP.getFreeHeap() adds up every free byte, but an allocation needs one contiguous run. After a few hundred String concatenations the heap is Swiss cheese: 30 KB "free", and no single hole bigger than 4 KB.

Log all three numbers right before the POST:

Serial.printf("free=%u maxblock=%u frag=%u%%\n",
              ESP.getFreeHeap(),
              ESP.getMaxFreeBlockSize(),
              ESP.getHeapFragmentation());

If maxblock is smaller than your TLS receive buffer, the allocation fails no matter how much total heap is left. That is the whole mystery, usually.

Quick detour: why a failed malloc becomes a reboot

Hang on, why does a failed allocation reset the chip rather than just return an error? Because somewhere between your code and the library, a pointer from new or malloc gets used without a check. The ESP8266 has no memory protection to catch it politely, so the CPU takes a load or store exception, and the default handler restarts it.

That is also why the reboot looks random. Whether the allocation fails depends on how fragmented the heap happens to be at that moment.

Shrink what TLS asks for

Two knobs matter. The first is the buffer size, which you can cut dramatically if the server supports Maximum Fragment Length Negotiation (MFLN). The core can probe for it, and you must set buffer sizes before connecting.

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>

bool postReading(const char *host, const char *path, const char *json) {
  BearSSL::WiFiClientSecure client;
  client.setInsecure();  // see the caveat below

  if (client.probeMaxFragmentLength(host, 443, 1024)) {
    client.setBufferSizes(1024, 1024);
  }
  if (!client.connect(host, 443)) {
    return false;
  }

  client.printf("POST %s HTTP/1.1\r\n"
                "Host: %s\r\n"
                "Content-Type: application/json\r\n"
                "Content-Length: %u\r\n"
                "Connection: close\r\n\r\n",
                path, host, (unsigned)strlen(json));
  client.print(json);

  // Read the status line, then drop the connection.
  String status = client.readStringUntil('\n');
  client.stop();
  return status.indexOf("200") > 0;
}

If the probe fails, the server does not support MFLN and you are stuck with the large buffer. Many big hosts do not offer it; a small server you control often can. Check before you design around it.

Stop building the body out of Strings

The second knob is your own code. Building a JSON body with String and += reallocates repeatedly, and every reallocation leaves a hole behind. Do it a few thousand times and the heap that TLS needs is gone.

  • Format into a fixed char array with snprintf, sized for the largest body you will send.
  • Create the array once, as a global or static, so it never touches the heap at all.
  • Write the request straight to the client with printf and print, as above, rather than assembling it first.
  • Create the secure client on demand and let it go out of scope, so its buffers are returned after each POST.

The last point deserves care. A long-lived WiFiClientSecure holds its buffers for as long as it exists, even when idle.

The security cost of setInsecure()

setInsecure() skips certificate validation. It saves memory and code, and it means anyone on the path can impersonate the server. That is fine for a bench test and poor for a sensor posting credentials or private readings.

The core offers cheaper options than a full trust store: pinning a known public key with setKnownKey(), or loading a single trust anchor with setTrustAnchors(). Either costs some memory, so measure again after switching. Also remember that the clock must be sane for certificate dates to validate, so set the time via NTP first.

Check the handshake is not tripping the watchdog

If the reset reason says "Soft WDT" rather than an exception, the problem is time, not memory. RSA handshakes on an 80 MHz core are slow, and any busy loop of your own around the connect call can starve the watchdog. Keep yield() or delay(0) in anything that waits, and prefer servers that use ECDSA certificates, which are quicker to verify.

A short checklist for the next reboot

  1. Print ESP.getResetInfo() at boot to separate exceptions from watchdog resets.
  2. Log getMaxFreeBlockSize() just before connecting, not only free heap.
  3. Probe MFLN and shrink buffers if the server allows it.
  4. Remove String churn from anything that runs repeatedly.
  5. Scope the secure client to the single request.

Most of the time the numbers from step two settle it within one reboot cycle. The heap was never full; it was just broken into pieces too small to use.