"Failed to create socket" on ESP32: You Have Run Out of LWIP Sockets

A voice assistant that went silent after a firmware update turned out to be an exhausted LWIP socket pool. How to recognise it, why the symptom misleads, and the one Kconfig value to change.

The bug report from the field was: “After I use the intercom, the voice assistant stops working. It just hangs.”

That sentence contains a wrong assumption, and the wrong assumption cost a full round of debugging in the wrong place. This article is about the real cause, but it is also about why the reported symptom pointed away from it.

Symptoms

E EspUdp: Failed to create socket

Root cause

Each voice conversation in this firmware opens a temporary UDP socket to the speech server for the audio stream. It is created when the session starts and closed when it ends.

The project’s sdkconfig had:

CONFIG_LWIP_MAX_SOCKETS=10

Over the previous few releases the number of long-lived sockets had crept up to nine or ten:

Persistent socket Count
MQTT over TLS (two brokers) 2
MJPEG streaming server 1-2
H.264 streaming server 1-2
Joystick / IMU control channel, UDP 8208 1
Intercom audio, UDP 8209 1
Assorted listeners 1-2

When the pool is full, the voice session cannot open its audio socket. The request to the server still goes out over the existing MQTT/WebSocket path, so the wake-up “works”. But the reply audio has nowhere to arrive, the session never sees an end-of-stream, and the state machine waits forever.

CONFIG_LWIP_MAX_SOCKETS = 10 MQTTTLS #1 MQTTTLS #2 MJPEGlisten MJPEGclient H.264listen H.264client UDP8208 UDP8209 OTA /HTTP DNS /misc voice session UDP socket socket() → -1, ENFILE = 16 voice 5 spare Rule: count persistent sockets before adding one. Same audit for LEDC (8), PCNT (4), RMT, timers.
Nine or ten persistent sockets in a pool of ten leaves nothing for the voice session's temporary UDP socket.

Fix: raise the pool.

CONFIG_LWIP_MAX_SOCKETS=16

The maximum for LWIP in ESP-IDF is 16 on most targets. Each socket costs a small amount of RAM whether or not it is in use, which is why the default is conservative.

Why the report was misleading

The release that added the intercom socket (v2.2.18) was the last straw that filled the pool. It also happened to be the release the user first tried the intercom on. So from their point of view: used intercom → assistant broke.

The truth was: upgraded to v2.2.18 → assistant broke, and it would have broken without touching the intercom at all.

Because of that framing, the previous debugging round went after a genuine but unrelated hazard — a race condition in how the microphone was handed between the intercom and the wake-word engine. That race was real and got fixed (strict serial handshake, talk_busy guard). It was not the cause of this bug.

Lessons

  1. Before adding any persistent socket, count the ones you already have. Write the list down; it takes two minutes.
  2. Read the serial log even when the symptom is “obviously” somewhere else. Nothing about “the speaker is silent and the state machine is stuck” suggests sockets. The log line did.
  3. Apply the same audit to every fixed-size resource pool. On the ESP32 that includes LEDC channels (8), PCNT units (4 on most targets), RMT channels, hardware timers, and I²C/SPI hosts. Running out of LEDC channels for motor PWM is what pushed my servo onto MCPWM — same category of problem, different pool.
  4. Correlation in time is not causation. “It broke when I used X” is evidence about when, not about why.