Skip to content

esp_websocket_client: Add Kconfig options for PSRAM allocation (IDFGH-17011)#980

Open
shardt68 wants to merge 1 commit into
espressif:masterfrom
shardt68:feature/websocket_psram_stack_config
Open

esp_websocket_client: Add Kconfig options for PSRAM allocation (IDFGH-17011)#980
shardt68 wants to merge 1 commit into
espressif:masterfrom
shardt68:feature/websocket_psram_stack_config

Conversation

@shardt68

@shardt68 shardt68 commented Dec 29, 2025

Copy link
Copy Markdown

Description

This PR introduces two new Kconfig options to the esp_websocket_client component to allow memory allocation in external RAM (PSRAM):

  1. ESP_WS_CLIENT_TASK_STACK_IN_EXT_RAM: Enables allocation of the WebSocket task stack in PSRAM using xTaskCreateStaticPinnedToCore.
  2. ESP_WS_CLIENT_ALLOC_IN_EXT_RAM: Enables allocation of the esp_websocket_client structure and its internal configuration storage in PSRAM.

Motivation

In applications requiring multiple concurrent secure connections (e.g., TLS/HTTPS), internal RAM is a critical bottleneck. Each TLS session typically requires 16-20KB of internal RAM for buffers. By allowing the WebSocket client's task stack and internal structures to be moved to PSRAM, significant internal memory is freed for these critical network operations, improving overall system stability and preventing ESP_ERR_NO_MEM failures in memory-constrained scenarios.

Implementation Details

  • Uses heap_caps_calloc with MALLOC_CAP_SPIRAM for the client structure and config.
  • Implements xTaskCreateStaticPinnedToCore for the task stack when PSRAM is selected.
  • Safety: Includes a fallback mechanism that reverts to internal RAM allocation if PSRAM allocation fails at runtime.
  • Compatibility: The default behavior remains unchanged (internal RAM allocation).

Related

  • Related to general ESP-IDF best practices for PSRAM-enabled devices (ESP32-S3, ESP32-WROVER) to mitigate internal RAM fragmentation and exhaustion during heavy network I/O.

Testing

  • Hardware: Tested on ESP32-S3 (8MB Octal PSRAM).
  • Scenario: Verified stable WebSocket connection while simultaneously performing multiple concurrent HTTPS/TLS requests to external APIs.
  • Memory Verification: Confirmed via heap_caps_get_free_size that internal RAM usage decreased by the expected amount (~5KB for structure + stack size).
  • Stability: Verified correct task startup and proper resource cleanup during client destruction.
  • Fallback Test: Verified that the client still initializes correctly on a device with PSRAM disabled/unavailable by falling back to internal RAM.

Note

Medium Risk
Changes FreeRTOS task teardown and allocation paths; incorrect ordering could cause use-after-free on the PSRAM stack, though the suspend/delete design targets that. PSRAM allocation failures fail hard (no fallback), which differs from the PR description’s claimed fallback behavior.

Overview
Adds two SPIRAM-gated Kconfig options so memory-heavy WebSocket client pieces can live in PSRAM instead of internal RAM, with defaults unchanged.

ESP_WS_CLIENT_ALLOC_IN_EXT_RAM allocates the client object and websocket_config_storage_t via heap_caps_calloc(..., MALLOC_CAP_SPIRAM) and frees them with heap_caps_free; failed allocation makes esp_websocket_client_init() return NULL (no internal-RAM fallback).

ESP_WS_CLIENT_TASK_STACK_IN_EXT_RAM (requires static task allocation) creates the worker with xTaskCreateStaticPinnedToCore: stack in PSRAM, TCB in internal RAM. On normal stop the task vTaskSuspends instead of self-deleting so teardown can vTaskDelete from another context before freeing the PSRAM stack; destroy_and_free_resources and esp_websocket_client_start after stop() implement that lifecycle. esp_websocket_client_destroy_on_exit() returns ESP_ERR_NOT_SUPPORTED when the PSRAM stack option is enabled.

Reviewed by Cursor Bugbot for commit 6423484. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant

CLAassistant commented Dec 29, 2025

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch 2 times, most recently from 13277dd to 27c4e4b Compare December 29, 2025 12:14
@github-actions github-actions Bot changed the title esp_websocket_client: Add Kconfig options for PSRAM allocation esp_websocket_client: Add Kconfig options for PSRAM allocation (IDFGH-17011) Dec 29, 2025
@espressif-bot espressif-bot added the Status: Opened Issue is new label Dec 29, 2025
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 27c4e4b to 8a75888 Compare December 29, 2025 12:40
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 8a75888 to b683518 Compare December 29, 2025 12:54
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from b683518 to 6c381b9 Compare December 29, 2025 13:03
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 6c381b9 to e84c0eb Compare December 29, 2025 15:43
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch 2 times, most recently from cbf6bad to 016bf62 Compare December 29, 2025 16:14
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 016bf62 to 0ae9008 Compare December 29, 2025 16:46
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch 2 times, most recently from 15cba04 to 9d9f1c9 Compare December 29, 2025 17:04
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 9d9f1c9 to 391fb0b Compare December 29, 2025 17:20
@shardt68

Copy link
Copy Markdown
Author

Hi @gabsuren,

I wanted to follow up on this PR to see if there is any feedback or if any additional information is needed from my side to move this forward.

These changes are quite important for memory-constrained applications using multiple concurrent TLS/HTTPS connections (like Spotify Connect implementations). By allowing the WebSocket task stack and client structure to be moved to PSRAM, we can significantly free up critical internal RAM.

Beyond the memory allocation, this PR also introduces a more robust lifecycle management (using DESTRUCTION_IN_PROGRESS_BIT and deferred task deletion). This hardening has proven to be very stable and effectively prevents race conditions during rapid reconnect/destroy cycles that were previously an issue.

The branch is currently conflict-free and has been extensively tested on ESP32-S3. Looking forward to your thoughts!

@gabsuren gabsuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some notes. Please take a look.
The task cleanup logic is duplicated across 4 different locations. This increases maintenance burden and the risk that future changes will introduce bugs if not applied consistently to all 4 spots.

@gabsuren

Copy link
Copy Markdown
Collaborator

@shardt68 Regarding the concurrency issues you encountered:
May I ask what specific deadlock issues you faced during development? Can you provide example code and config so we can trace it?
This will help us design a safer hardening fix in a separate PR.

Thank you again!
Suren

Comment thread components/esp_websocket_client/esp_websocket_client.c
Comment thread components/esp_websocket_client/esp_websocket_client.c
@shardt68

Copy link
Copy Markdown
Author

Hi @gabsuren,

Thank you for the guidance! I have updated the PR to focus strictly on the PSRAM allocation configuration. Specifically, I have:

Reverted the version bump in idf_component.yml and CHANGELOG.md.
Removed the concurrency hardening (deferred destruction, state bits, etc.).
Removed the dynamic buffer feature as it was out of scope for the PSRAM requirement.
Preserved the MALLOC_CAP_SPIRAM allocation for the client structure and the static task stack support.
Regarding your question about the deadlocks, here are the primary scenarios we encountered during development:

  1. Self-Destruction Deadlock (Task Context)
    The most critical deadlock occurs when esp_websocket_client_destroy() is called from within the client's own event handler (e.g., in response to an error).

Example Scenario:

static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
    if (event_id == WEBSOCKET_EVENT_ERROR) {
        /* 
         * ❌ DEADLOCK: esp_websocket_client_destroy calls stop_wait_task() 
         * which waits for the STOPPED_BIT. 
         * The task is currently blocked in this callback, so it never reaches 
         * the end of its loop to set the STOPPED_BIT.
         */
        esp_websocket_client_destroy(client); 
    }
}
  1. Task Lifecycle Race (Rapid Start-Stop)
    When calling stop() followed immediately by start(), we found that client->task_handle could become inconsistent. If the previous task had not yet reached its exit point (vTaskDelete), the new xTaskCreate call would overwrite the handle while the old task was still cleaning up the transport or internal resources.

  2. API Handle "Check-then-use" Race
    In multi-threaded environments, one task might call esp_websocket_client_send_text() while another calls stop(). Previously, if a context switch occurred after the connection check but before the transport write, the system could attempt to use a transport handle that was just closed or invalidated.

I have already prepared these hardening fixes in a separate branch and will submit them as a new PR for independent review once this PSRAM feature is addressed.

Please let me know if this version looks good for merging!

Best regards,
shardt68

@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from d2242b7 to 61a8100 Compare January 30, 2026 21:04
Comment thread components/esp_websocket_client/esp_websocket_client.c
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@gabsuren

gabsuren commented Feb 3, 2026

Copy link
Copy Markdown
Collaborator

@shardt68 thank you for simplifying the PR to consider only the PSRAM part.
Left some comments for your consideration.

Best regards,
Suren

@gabsuren

Copy link
Copy Markdown
Collaborator

Hi @gabsuren,

Thank you for the guidance! I have updated the PR to focus strictly on the PSRAM allocation configuration. Specifically, I have:

Reverted the version bump in idf_component.yml and CHANGELOG.md. Removed the concurrency hardening (deferred destruction, state bits, etc.). Removed the dynamic buffer feature as it was out of scope for the PSRAM requirement. Preserved the MALLOC_CAP_SPIRAM allocation for the client structure and the static task stack support. Regarding your question about the deadlocks, here are the primary scenarios we encountered during development:

  1. Self-Destruction Deadlock (Task Context)
    The most critical deadlock occurs when esp_websocket_client_destroy() is called from within the client's own event handler (e.g., in response to an error).

Example Scenario:

static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
    if (event_id == WEBSOCKET_EVENT_ERROR) {
        /* 
         * ❌ DEADLOCK: esp_websocket_client_destroy calls stop_wait_task() 
         * which waits for the STOPPED_BIT. 
         * The task is currently blocked in this callback, so it never reaches 
         * the end of its loop to set the STOPPED_BIT.
         */
        esp_websocket_client_destroy(client); 
    }
}
  1. Task Lifecycle Race (Rapid Start-Stop)
    When calling stop() followed immediately by start(), we found that client->task_handle could become inconsistent. If the previous task had not yet reached its exit point (vTaskDelete), the new xTaskCreate call would overwrite the handle while the old task was still cleaning up the transport or internal resources.
  2. API Handle "Check-then-use" Race
    In multi-threaded environments, one task might call esp_websocket_client_send_text() while another calls stop(). Previously, if a context switch occurred after the connection check but before the transport write, the system could attempt to use a transport handle that was just closed or invalidated.

I have already prepared these hardening fixes in a separate branch and will submit them as a new PR for independent review once this PSRAM feature is addressed.

Please let me know if this version looks good for merging!

Best regards, shardt68

@shardt68 sorry I missed your last message, thanks for providing an example.
Actually, this is a known constraint of the task lifecycle here :)

The deadlock occurs because esp_websocket_client_destroy() attempts to stop the WebSocket task and waits for it to finish. However, when you call this function from within the event handler (e.g., WEBSOCKET_EVENT_ERROR), you are running inside that very task. The task essentially waits for itself to finish, causing a deadlock.

Instead of destroying the client immediately, signal it to destroy itself upon exit.

❌ Incorrect (Causes Deadlock):

static void websocket_event_handler(...) {
    if (event_id == WEBSOCKET_EVENT_ERROR) {
        // DEADLOCK: Waits for this task to stop while running inside it
        esp_websocket_client_destroy(client); 
    }
}

✅ Correct:
Use esp_websocket_client_destroy_on_exit(). This sets a flag so the task cleans up its own resources gracefully when it exits the loop.

static void websocket_event_handler(...) {
    if (event_id == WEBSOCKET_EVENT_ERROR) {
        // Safe: Signals the task to clean up after it breaks the loop
        esp_websocket_client_destroy_on_exit(client);
    }
}

Best regards,
Suren

Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 requested a review from gabsuren March 23, 2026 15:56
@cursor

cursor Bot commented Mar 31, 2026

Copy link
Copy Markdown

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@gabsuren gabsuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @shardt68, thanks for iterating on this, the strict-allocation direction is good. A few things to address before merge

  1. Commits

Please rebase onto current master and squash the 7 commits into a single conventional commit, for example:

feat(websocket): add Kconfig options for PSRAM allocation

  1. Strict allocation is not fully strict yet

In esp_websocket_client_start(), the else { xTaskCreatePinnedToCore(...) } branch silently falls back to internal RAM when task_stack <= 0.

Since the user explicitly enabled PSRAM task-stack allocation, I think the behavior should be:

PSRAM stack allocation succeeds, or start fails with a clear error.

Please remove the fallback branch so the option consistently means “allocate the task stack in PSRAM, or fail”.

  1. Kconfig documentation / dependencies

Please document and enforce the expected dependencies:

  • depends on SPIRAM
  • depends on FREERTOS_SUPPORT_STATIC_ALLOCATION for the task-stack option
  • mention that only the task stack moves to PSRAM; the TCB stays in internal RAM
  • mention the allocation-failure behavior: return NULL / ESP_FAIL, no fallback to internal RAM

Looks good otherwise, once these are in, happy to take another pass. Thanks!

Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch 2 times, most recently from 09ec076 to 2c3347f Compare May 29, 2026 13:18
@gabsuren gabsuren self-requested a review June 17, 2026 08:21
@gabsuren

gabsuren commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

@shardt68 thank you for the updates. Please fix the pre-commit check and address the minor notes I left. Other than that, I think the PR is mostly ready for merging.

There is one remaining major issue with CONFIG_ESP_WS_CLIENT_TASK_STACK_IN_EXT_RAM enabled: the direct dangerous case is destroy_on_exit. When selected_for_destroying is true, the WebSocket task can call destroy_and_free_resources() from inside itself, which frees its own static stack/TCB before calling vTaskDelete(NULL).

This was not an issue before because the task stack/TCB were owned by FreeRTOS. With the new external-stack path, they are client-owned buffers, so freeing them from the running task can lead to use-after-free or memory corruption.

I think this should be fixed before enabling CONFIG_ESP_WS_CLIENT_TASK_STACK_IN_EXT_RAM, or the limitation should at least be documented and destroy_on_exit should be prevented/unsupported in this mode untill the fix.

Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
Comment thread components/esp_websocket_client/esp_websocket_client.c
Comment thread components/esp_websocket_client/esp_websocket_client.c
Comment thread components/esp_websocket_client/esp_websocket_client.c
Comment thread components/esp_websocket_client/esp_websocket_client.c
@shardt68

Copy link
Copy Markdown
Author

I addressed the remaining Bugbot findings.

PSRAM allocations freed with free
Fixed by using matching deallocation for client/config objects:
heap_caps_free when CONFIG_ESP_WS_CLIENT_ALLOC_IN_EXT_RAM is enabled
free otherwise
Applied consistently in init error path and teardown paths.
Start failure leaks PSRAM task buffers
Updated start() error path in PSRAM stack mode to free task_stack_buffer/task_buffer immediately and clear pointers, instead of keeping them until destroy.
Additionally, the previous high-severity race around PSRAM stack free timing was already handled in commit c6a31b2 (yield before freeing stack/TCB buffers).

Please take another look.

@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 2c71d70 to 7e14cb1 Compare June 29, 2026 15:08
Comment thread components/esp_websocket_client/esp_websocket_client.c Outdated
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from f2c4c09 to 6e9a74f Compare July 14, 2026 12:55

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6e9a74f. Configure here.

Comment thread components/esp_websocket_client/esp_websocket_client.c
@shardt68 shardt68 force-pushed the feature/websocket_psram_stack_config branch from 73b36ba to 6423484 Compare July 14, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants