STM32 Fixed Memory Pool for Networking using Mongoose
In this tutorial, we will limit the network memory usage of the Mongoose networking library to a fixed, preallocated memory pool. This ensures that networking never eats into the RAM reserved for other parts of your application.
We will use O1heap — a fast, deterministic memory allocator — to manage a fixed 40 KB RAM block. Mongoose will use this block exclusively for all network operations. We will also use the real-time graphs from the previous tutorial to monitor live memory usage and the number of active connections on a web dashboard.
This is Part 6 of the STM32 TCP/IP with Mongoose series. The earlier parts are listed below:
- STM32 TCP/IP with Mongoose: Complete Setup Guide
- STM32 HTTP Webserver using Mongoose
- STM32 Weather Station using Mongoose
- STM32 OTA Firmware Update using Mongoose
- STM32 Real-Time Graph using Mongoose

How Fixed Memory Pool Works with Mongoose
When Mongoose runs on an embedded system, it allocates and frees memory dynamically as connections are opened and closed. By default, it pulls from the general heap — which is shared with the rest of your application. If a heavy network load or a memory leak happens, it can starve other parts of your firmware.
The fix is to give Mongoose its own dedicated block of memory. Mongoose supports this through a custom memory allocator hook. Once you plug in your own mg_calloc and mg_free, Mongoose will only allocate from wherever you tell it to — in our case, a fixed array in RAM.
Why Limit Network Memory on Embedded Systems
Embedded systems have limited RAM, and that RAM is shared between your application logic, stack, heap, and peripherals. If networking is allowed to grow without limits, a burst of incoming connections or large payloads can crash the system or cause undefined behavior.
By capping the network memory to a fixed pool, you get:
- Predictable memory behavior — the network can never take more than you allow
- System stability — even under heavy load, your non-network code always has its RAM
- Easier debugging — out-of-memory conditions in the network are isolated and visible
In this tutorial, we allocate 40 KB for networking. When that pool is full, new connections are rejected gracefully. Existing connections continue to work.
What Is O1heap and Why We Use It
O1heap is a memory allocator designed specifically for hard real-time and safety-critical embedded systems. Its main properties are:
- O(1) allocation and free — the time it takes is constant, regardless of heap state
- Minimal overhead — small metadata, no fragmentation over time with correct usage
- No external dependencies — it is a pair of single
.cand.hfiles
We need just two files from the O1heap GitHub repository: o1heap.c and o1heap.h. There are no FreeRTOS dependencies, no extra headers, and no configuration file. This makes it much easier to integrate than alternatives like the FreeRTOS heap implementations.
How Mongoose Uses Custom Memory Allocators
Mongoose allocates memory through two internal functions: mg_calloc() and mg_free(). These are defined inside mongoose.c by default. Mongoose lets us replace these with our own versions by setting a build option in the configuration file.
Once we define MG_ENABLE_CUSTOM_CALLOC 1 in mongoose_config.h, the default implementations are removed. The compiler will then expect us to provide our own mg_calloc and mg_free. Inside those functions, we route all allocations to O1heap.
The flow looks like this:
Mongoose allocates memory
→ calls mg_calloc()
→ our custom mg_calloc() calls o1heapAllocate()
→ memory comes from our fixed 40 KB bufferMongoose Dashboard – Designing the UI
We start with the LED Toggle dashboard generated in a previous tutorial. This already has the LED control panel and basic structure in place. We will add two memory graphs and a connection gauge on top of it.
Create a new project in Mongoose Wizard — select the STM32H745 Discovery board, CubeIDE as the build environment, and Bare Metal for RTOS. Start from the LED Toggle dashboard template.
Creating the Memory Graphs and Connection Gauge
Add a new container to the dashboard. Inside this container, add two graph elements — one for memory used and one for free memory. Both graphs show the last 30 seconds of data and update every 25 milliseconds, which gives a near real-time view of the memory state.
Since we are allocating 40 KB for networking, set the Y-axis scale to 40000 for both graphs. This keeps both graphs on the same scale, making it easy to see used + free = 40,000 at a glance.
{"xrange":30,"updateMs":25,"uplot":{"scales":{"y":{"auto":false,"min":0,"max":40000}}}}- “xrange”: 30
We set the X-axis range to 30 sec. The graph will display data for the last 30 seconds, basically real time updates only. - “updateMs”: 25
The graph updates every 25 ms. It fetches new data from the API at this interval. - “auto”: false
We disable automatic scaling of the Y-axis. - “min”: 0, “max”: 40000
We manually set the Y-axis range from 0 to 40000. This is useful to display the memory upto 40KB.
This setup ensures the graph shows a fixed range with controlled updates, which makes the visualization stable and easy to read.
Along with the graphs, add a gauge element to the LED control panel area. This gauge will show the number of active connections in real time. Set the gauge range from 0 to 24, with ticks at every 4 connections.
Creating the Stats REST API Endpoint
The LED endpoint is already present. We need to add a new endpoint called stats that carries three values:
ram_occupied— memory currently used from the pool (integer)ram_free— memory remaining in the pool (integer)num_connections— number of active Mongoose connections (integer)
Set this endpoint to read-only, since the web UI only reads from the MCU. It does not write anything back.
The Mongoose wizard will auto-generate a struct stats and a getter function signature in mongoose_glue.c:
struct stats {
int ram_occupied;
int ram_free;
int num_connections;
};Configuring WebSocket Updates
Go to the Settings tab and enable WebSocket. Also set the auto-update interval to 1 second. This activates the heartbeat mechanism — the page will refresh its data every second automatically.
Now go to the Page Content tab and assign the API variables to their elements:
stats.ram_occupied→ memory usage graphstats.ram_free→ free memory graphstats.num_connections→ connection gauge
Generate the project and import it into STM32 CubeIDE.
O1heap Integration, Setup, Code & Results
With the project imported, we now need to add the O1heap allocator, hook it into Mongoose, write the getter functions, and configure the GPIO pins for the LEDs. Let us go through each step.
Adding O1heap to the Project
Go to the O1heap GitHub repository. Open o1heap.c and copy the raw content. Create a new source file inside the Src folder of your CubeIDE project and paste the content. Do the same for o1heap.h — create a header file inside Inc and paste the header content.
The image below shows the project structure with o1heap library added.
Defining Custom mg_calloc and mg_free Functions
Open mongoose_config.h and add the following line:
#define MG_ENABLE_CUSTOM_CALLOC 1If you build the project now, the compiler will report an error about mg_calloc being undefined. That is expected — we just told Mongoose we are providing our own version, and now we need to write it.
Open main.c. Include the O1heap header at the top:
#include "o1heap.h"Now define the memory pool. We use a 40 KB buffer, aligned to O1HEAP_ALIGNMENT:
#define O1HEAP_SIZE 40*1024
static uint8_t o1heap_buffer[O1HEAP_SIZE] __attribute__((aligned(O1HEAP_ALIGNMENT)));
static O1HeapInstance *heap_instance = NULL;Add an initialization function that sets up the heap before Mongoose starts:
void o1heap_Init (void)
{
o1heap = o1heapInit(o1heap_buffer, sizeof(o1heap_buffer));
if (o1heap == NULL)
{
// failed to initialize, check alignement
}
}Call o1heap_Init() inside main() before any peripheral or Mongoose initialization:
o1heap_Init();Now write the custom mg_calloc and mg_free functions. You can find the default function signatures inside mongoose.c — copy those signatures and implement them in main.c:
void *mg_calloc(size_t count, size_t size) {
size_t total = count * size;
void *ptr = o1heapAllocate(o1heap, total);
if (ptr) {
memset(ptr, 0, total);
}
return ptr;
}
void mg_free(void *ptr) {
o1heapFree(o1heap, ptr);
}mg_calloc allocates the memory and zeroes it out, just like calloc is supposed to. mg_free simply releases it back to the pool. Mongoose calls both of these internally — we never call them directly.
Writing the Stats Getter and LED Functions
Open mongoose_glue.c and find the auto-generated getter for stats. We should not edit this file directly — copy the function name and write our own version in main.c.
The O1heap library provides a o1heapGetDiagnostics() function that returns a structure with details about the heap state. We use this to fill in the stats:
void my_get_stats(struct stats *data)
{
O1HeapDiagnostics diag = o1heapGetDiagnostics(o1heap);
data->ram_occupied = diag.allocated;
data->ram_free = diag.capacity - diag.allocated;
data->num_connections = 0;
for (struct mg_connection *c = g_mgr.conns; c != NULL; c = c->next)
{
data->num_connections++;
}
}The mgr here is the Mongoose event manager instance. We count connections by walking the linked list of active connections.
For the LED controls, we first need to configure the GPIO pins in CubeMX. Open STM32CubeMX, search for pins PI8 and PE6, and configure both as GPIO output. Since this is a dual-core board, open the GPIO settings and assign both pins to the Cortex-M7 context. Regenerate the project.
Now write the LED getter function:
void my_get_leds(struct leds *data){
data->led1 = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6);
data->led2 = HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_8);
}And the LED setter function:
void my_set_leds(struct leds *data) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, data->led1);
HAL_GPIO_WritePin(GPIOI, GPIO_PIN_8, data->led2);
}Registering HTTP Handlers and WebSocket Reporter
After mongoose_init(), register the HTTP handlers and start the WebSocket reporters:
mongoose_set_http_handlers("stats", my_get_stats, NULL);
mongoose_set_http_handlers("leds", my_get_leds, my_set_leds);
mongoose_add_ws_reporter(100, "stats");The stats reporter pushes data every 100 milliseconds, which keeps the graphs smooth and near real-time. The LED endpoint uses both a getter and a setter since the UI can both read and write LED state.
Make sure the endpoint names — "stats" and "leds" — exactly match what you defined in the Mongoose wizard.
Full Code
Here is the complete set of additions made to main.c:
#include "o1heap.h"
#include <string.h>
/* ---- Memory Pool ---- */
#define O1HEAP_SIZE 40*1024
static uint8_t o1heap_buffer[O1HEAP_SIZE] __attribute__((aligned(O1HEAP_ALIGNMENT)));
static O1HeapInstance *heap_instance = NULL;
void o1heap_Init (void)
{
o1heap = o1heapInit(o1heap_buffer, sizeof(o1heap_buffer));
if (o1heap == NULL)
{
// failed to initialize, check alignement
}
}
/* ---- Custom Mongoose Allocator ---- */
void *mg_calloc(size_t count, size_t size) {
size_t total = count * size;
void *ptr = o1heapAllocate(o1heap, total);
if (ptr) {
memset(ptr, 0, total);
}
return ptr;
}
void mg_free(void *ptr) {
o1heapFree(o1heap, ptr);
}
/* ---- Stats Getter ---- */
void my_get_stats(struct stats *data)
{
O1HeapDiagnostics diag = o1heapGetDiagnostics(o1heap);
data->ram_occupied = diag.allocated;
data->ram_free = diag.capacity - diag.allocated;
data->num_connections = 0;
for (struct mg_connection *c = g_mgr.conns; c != NULL; c = c->next)
{
data->num_connections++;
}
}
/* ---- LED Getter ---- */
void my_get_leds(struct leds *data){
data->led1 = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6);
data->led2 = HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_8);
}
/* ---- LED Setter ---- */
void my_set_leds(struct leds *data) {
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, data->led1);
HAL_GPIO_WritePin(GPIOI, GPIO_PIN_8, data->led2);
}
/* ---- Inside main() ---- */
// Call before any peripheral init:
/* USER CODE BEGIN SysInit */
o1heap_Init();
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ETH_Init();
MX_USART3_UART_Init();
MX_RNG_Init();
/* USER CODE BEGIN 2 */
mongoose_init();
mongoose_set_http_handlers("stats", my_get_stats, NULL);
mongoose_set_http_handlers("leds", my_get_leds, my_set_leds);
mongoose_add_ws_reporter(100, "stats");
for (;;) {
mongoose_poll();
}Output
Serial Console
Mongoose will initialize and print the DHCP-assigned / Static IP address. Initially, the logs will show normal heartbeat messages.
The image below shows the serial console output after flashing — Mongoose initialized and IP address assigned.
Once the memory pool fills up, you will see out-of-memory messages in the logs. This is expected and correct behavior — Mongoose is respecting the memory limit we set.
The image below shows the serial console with out-of-memory log messages after the pool is exhausted.
Web UI Dashboard
Open the assigned IP in a browser. The dashboard loads with the LED controls, two memory graphs, and the connection gauge.
The image below shows the web dashboard with real-time memory usage and free memory graphs updating live.
At baseline, memory usage is around 28 KB and free memory is around 12 KB. The number of active connections starts at 6 — this includes the HTTP and HTTPS listeners, the WebSocket connection, the heartbeat API call, and a few internal connections Mongoose maintains.
As we open more browser tabs, each new connection consumes more memory. The memory usage climbs toward 35 KB, then 37 KB, while free memory drops accordingly.
The image below shows the graphs as more browser tabs are opened — memory usage rises and free memory decreases.
Once the pool is fully exhausted, Mongoose stops accepting new connections. Closing existing tabs frees memory — you can see the usage graph drop and the free memory graph rise again in real time.
This is exactly what we want: a hard cap on network memory that keeps the rest of the system safe.
STM32 Fixed Memory Pool for Networking using Mongoose — Video Tutorial
This video walks through the complete implementation of a fixed memory pool for Mongoose networking on STM32. We integrate the O1heap allocator, hook it into Mongoose’s custom allocator interface, and monitor real-time RAM usage and active connections on a live web dashboard using WebSocket graphs.
Download STM32 Fixed Memory Pool (Mongoose) Project Files
Complete CubeIDE project with O1heap integration, custom Mongoose allocator, real-time memory graphs, and LED control. Free to download — support the work if it helped you.
STM32 fixed memory pool — Frequently Asked Questions
Yes, heap 4 works as an allocator, but it pulls in several FreeRTOS header dependencies. If your project is bare-metal (no FreeRTOS), resolving those dependencies requires extra setup. O1heap is just two files with no dependencies, which makes it much easier to drop in.
No. When o1heapAllocate cannot satisfy a request, it returns NULL. Our mg_calloc returns NULL to Mongoose, which then rejects the new connection gracefully. The existing connections continue working normally. You will see out-of-memory messages in the serial log, but the system keeps running.
Yes. Change the NETWORK_HEAP_SIZE define in main.c. Just make sure your MCU actually has that much free RAM. On the STM32H745, you have plenty of headroom. On smaller devices like the STM32F103, you may need to use a much smaller pool.
Mongoose opens several internal connections at startup — the HTTP listener, HTTPS listener, WebSocket connection, and a few internal management connections. These are always present. Browser tabs add on top of this base count.
o1heap_Init() before or after MX_GPIO_Init()?It does not matter for GPIO, but it must be called before mongoose_init(). If Mongoose tries to allocate memory before the heap is initialized, the heap_instance pointer will be NULL and all allocations will fail. Placing the call at the very top of main() before any other initialization is the safest approach.
Conclusion
In this tutorial, we limited Mongoose's network memory usage to a fixed 40 KB RAM pool using the O1heap allocator. The key idea is simple — instead of letting Mongoose pull from the shared heap freely, we gave it its own dedicated block of memory and plugged in a custom mg_calloc and mg_free to route all allocations through O1heap.
We could see this working live on the dashboard. As more browser tabs opened, memory usage climbed. Once the pool was exhausted, Mongoose stopped accepting new connections — but the system kept running. No crash, no undefined behavior. And as tabs were closed, the memory came back. The graphs made all of this visible in real time, which is a nice way to validate that the memory constraint is actually working as expected.
This approach works on any STM32 with enough RAM. You just change the NETWORK_HEAP_SIZE define to whatever fits your system. On tighter devices, even a 10–15 KB pool is enough for a few simultaneous connections.
Browse More STM32 Mongoose TCP/IP Tutorials
STM32 Web Server using Mongoose: HTTP, WebSocket & ADC Dashboard
STM32 Weather Station Web Server using Mongoose (Ethernet + BME280)
STM32 OTA Firmware Update using Mongoose – Step by Step Guide
STM32 Real-Time Graph using Mongoose Ethernet
STM32 Mongoose with W5500 – Ethernet on Any STM32 MCU
STM32 Modbus TCP Server – Read Discrete Inputs with Mongoose
STM32 Modbus TCP Server – Read and Write Coils using Mongoose
Arun is an embedded systems engineer with 10+ years of experience in STM32, ESP32, and AVR microcontrollers. He created ControllersTech to share practical tutorials on embedded software, HAL drivers, RTOS, and hardware design — grounded in real industrial automation experience.
Recommended Tools
Essential dev tools
Categories
Browse by platform











