--- title: ESP32-S3 without PSRAM: HTTPS fetch returns 0 bytes or connect() fails "connection refused" (TLS out of memory) date: 2026-06-14 summary: On a PSRAM-less ESP32-S3 (M5Stack Cardputer-ADV) an HTTPS fetch returned 0 bytes and later TLS connects failed with "connection refused". mbedTLS could not get a large enough contiguous heap block. Freeing the audio decoder, JSON document and 64 KB display sprite before TLS fixed it. tags: esp32, arduino, wifi, m5stack, cardputer environment: M5Stack Cardputer-ADV (ESP32-S3, no PSRAM, about 320 KB SRAM); Arduino via PlatformIO, platform espressif32@6.9.0 (ESP-IDF 4.4); M5Cardputer + M5Unified; ESP8266Audio 1.9.7 author: shaun, written up with Claude status: published --- ## Symptoms An Arduino MP3 player sketch on an M5Stack Cardputer-ADV fetches a JSON feed over HTTPS, then downloads several files over HTTPS to the microSD card. On this board: - the feed fetch **returned 0 bytes**, and - once the feed had been parsed, the per-file downloads failed because `client.connect()` reported **"connection refused"**, although the server was fine. Nothing was wrong with the network or the server. Both were memory failures that do not look like memory failures. (The "connection refused" text is what the sketch's connect failure reported; the exact log line was not recorded.) ## Root cause The Cardputer-ADV's ESP32-S3 has **no PSRAM**, only about 320 KB of internal SRAM, and a TLS handshake needs large contiguous heap blocks. With the audio decoder, the audio output object, a parsed JSON document and a 64 KB display sprite all allocated, only about **10 KB contiguous** was left after the mbedTLS handshake, and new TLS connections could not get the buffers they needed. The number that settled it was the **largest free heap block** before connecting: about **33 KB → TLS fails**, about **64 KB → TLS works**. To watch it yourself, log `heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)` (or `ESP.getMaxAllocHeap()`) just before `client.connect()`; total free heap is misleading here because fragmentation is the problem. ## Fix Free the big allocations before any TLS work, and keep large data out of RAM: 1. **Free the MP3 decoder and the audio output object** before starting TLS. 2. **Stream the response to a temporary file on SD**, then parse the JSON from the file, instead of reading the whole body into RAM. 3. **Free the JSON document** before the per-file downloads. Without this step, `client.connect()` fails with "connection refused". 4. **Delete the 64 KB display sprite** for the duration of Wi-Fi/TLS. It turned out to be the single largest allocation: once the app grew a little (more globals), steps 1–3 alone were no longer enough. While the sprite is gone, draw status text straight to the display. ```cpp sprite.deleteSprite(); // M5Canvas back buffer, ~64 KB // ... connect, download, write to SD; draw progress directly on the display ... ESP.restart(); // come back up clean and recreate the sprite ``` Restarting at the end of the sync is a simple way to get every freed object (decoder, sprite) back in a known state. With all four steps, the largest free block went from about 33 KB to about 64 KB and the HTTPS fetches and downloads worked.