Mongoose event-driven architecture

Mongoose is a production-grade TCP/IP stack with HTTP server, WebSocket, TLS/HTTPS, MQTT, and OTA firmware updates for STM32, NXP, RP2350, ESP32, and other Cortex-M and RISC-V microcontrollers.

What is Mongoose

Mongoose is an embedded networking library and application framework written in C. It runs on microcontrollers, embedded Linux devices, and desktop systems and is widely used to implement embedded web servers, device dashboards, REST APIs, cloud connectivity, and secure TLS-enabled services.

The library is intentionally small and easy to integrate. The entire networking stack lives in just two files, mongoose.c and mongoose.h

Mongoose provides built-in TCP/IP and TLS stacks and can also run on top of existing networking stacks such as lwIP, FreeRTOS+TCP, Zephyr, Netx and others.

Most TCP/IP stacks stop at the socket layer and expose the BSD sockets API, with ad-hoc examples for applications like web server and MQTT client. Mongoose goes further and provides a well-designed API for higher-level protocols such as HTTP, WebSocket, and MQTT. This makes it particularly well suited for implementing embedded web servers and network-enabled devices.

Internally Mongoose is built around a simple idea - event-driven networking.

Mongoose is widely used on microcontrollers such as STM32, ESP32, RP2040, NXP, and similar devices to implement embedded web servers, REST APIs, WebSocket dashboards, MQTT connectivity, firmware OTA updates and TLS-secured device communication.

Event-driven networking model

In the event-driven networking model the application does not call blocking functions like recv() or send() and wait for data.

Instead the application registers an event handler and reacts to network events generated by the library.

Events represent things like a new connection, incoming data, an HTTP request, a WebSocket message, or a closed connection.

This approach allows a single thread to manage many connections simultaneously. The library handles connection management, scheduling, and protocol parsing while the application focuses on device logic.

For embedded systems this model is extremely efficient. It keeps memory usage low, removes the need for threads, and keeps the control flow simple.

Events vs BSD sockets

Traditional networking APIs are based on BSD sockets. The sockets interface exposes functions like accept(), recv(), and send().

Applications using sockets must manage connection state, blocking behaviour, and multiplexing themselves. As applications grow and need to handle many connections, socket code becomes complex.

Event-driven networking solves this by building a higher-level abstraction on top of sockets.

Instead of calling socket functions directly, the application reacts to events such as connection open, data received, or connection closed.

Mongoose follows this model. Internally it uses sockets provided by the operating system or TCP/IP stack, but externally it exposes a clean event-driven interface where events are generated by the Mongoose event manager.

The following video explains the history of BSD sockets and how socket-based networking naturally evolves into an event-driven model.

Event loop and event handlers

Mongoose networking is built around two core structures:

The event manager owns the list of active connections and drives the networking system through the event loop. A typical embedded application repeatedly calls mg_mgr_poll().

Below is a minimal example of an application that implements an HTTP/HTTPS server:

#include "mongoose.h"

static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_MSG) {
    mg_http_reply(c, 200, "", "Hello world\n");
  }
}

int main(void) {
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  
  mg_http_listen(&mgr, "http://0.0.0.0:8000", http_ev_handler, NULL);
  mg_http_listen(&mgr, "https://0.0.0.0:8443", http_ev_handler, NULL);
  
  for (;;) {
    mg_mgr_poll(&mgr, 100);
  }
  return 0;
}

Each call to mg_mgr_poll() polls the network, processes incoming and outgoing packets, accepts new connections, and generates events for active connections.

Generating an event means calling an event handler function associated with a connection when something interesting happens - for example when a connection is accepted, data arrives, or outgoing data is sent.

When one of the listening connections accepts a new client, a new mg_connection object is created and added to the event manager's list of active connections.

The accepted connection, also called an inbound connection, buffers incoming data. When a full HTTP request is received, Mongoose parses it and calls the user-defined http_event_handler() with the MG_EV_HTTP_MSG event.

Connection objects

Every active network connection in Mongoose is represented by a structure called struct mg_connection. A connection object stores connection state, protocol-specific data, send and receive buffers, flags describing connection state, and a pointer to the event handler associated with that connection.

Connections are created when a listening server accepts an incoming connection or when the application initiates an outbound connection.

Connections are created when a listening server accepts an incoming connection or when the application initiates an outbound connection.

Once a connection exists, the event manager delivers events to that connection whenever network activity occurs.

Connection flags describe the state of the connection and control behaviour such as closing connections, listening sockets, or pending outgoing data.

This design allows the library to manage many connections simultaneously while keeping the application interface simple.

Send and receive buffers

Each connection maintains two buffers - a receive buffer and a send buffer.

Incoming network data is appended to the receive buffer and then MG_EV_READ gets triggered. Protocol handlers parse the data and generate higher-level events for the application.

Outgoing data is placed into the send buffer and transmitted when the network becomes ready. Sneding data is done through helper functions such as mg_send() or mg_http_reply() rather than manipulating buffers directly.

This buffering mechanism allows Mongoose to handle partial packets, asynchronous sending, and high-throughput communication.

Event handler function

Each connection has an event handler function. The handler defines how the connection behaves.

The handler receives three arguments:

  • struct mg_connection *c - the connection that generated the event
  • int ev - event identifier
  • void *ev_data - event-specific data

Example - simple TCP echo server:

static void ev_handler(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_READ) {
    mg_send(c, c->recv.buf, c->recv.len);
    c->recv.len = 0;
  }
}

Most Mongoose applications consist of event handlers reacting to events.

Typical events include:

  • MG_EV_CONNECT - outbound connection established
  • MG_EV_ACCEPT - inbound connection accepted
  • MG_EV_READ - data received
  • MG_EV_WRITE - data sent
  • MG_EV_CLOSE - connection closed

Protocol layers generate additional events such as:

  • MG_EV_HTTP_MSG - HTTP request or response received
  • MG_EV_WS_MSG - WebSocket message received
  • MG_EV_MQTT_MSG - MQTT publish received

Protocol handlers

Mongoose implements several networking protocols on top of its event-driven core. Examples include HTTP, WebSocket, MQTT, and raw TCP or UDP communication. Protocol handlers interpret incoming network data and generate higher-level events that the application can process. For example an HTTP request generates the event MG_EV_HTTP_MSG. WebSocket frames generate MG_EV_WS_MSG events. MQTT messages generate MQTT-related events.

This architecture allows applications to work with protocol-level events instead of parsing raw packets manually. Internally it is implemented by two event handlers inside each connection object:

  • c->pfn - protocol handler
  • c->fn - user defined handler

The protocol handler parses incoming data and generates protocol-specific events. For example, mg_http_listen() installs the HTTP protocol handler. It parses HTTP requests and emits MG_EV_HTTP_MSG. The user handler runs afterward and implements the application logic.

Architecture diagram

The Mongoose networking architecture is built from the following components:

  • the event loop (mg_mgr_poll) that processes network activity (Core)
  • connection objects (struct mg_connection) (Core)
  • the event manager (struct mg_mgr) (Core)
  • protocol handlers for HTTP, WebSocket, MQTT, and other protocols
  • application event handlers that define connection behavior
  • send and receive buffers used for network data
Mongoose networking architecture diagram

The diagram shows how Mongoose sits between the application and the network stack.
Application code implements event handlers, while Mongoose provides the networking core and protocol parsers.
Below Mongoose runs either an existing TCP/IP stack (lwIP, Zephyr, NetX, OS networking) or the built-in Mongoose TCP/IP stack.

Mongoose can run on top of existing TCP/IP stacks such as lwIP, Zephyr, NetX, or operating system stacks on Windows, macOS, and Linux.

It can also run stand-alone. Mongoose includes its own TCP/IP stack and network drivers, which makes it suitable for bare-metal systems without an operating system.

TLS integration follows the same model. Mongoose can use external TLS libraries such as OpenSSL or mbedTLS, or it can use its own built-in TLS 1.3 implementation.

This allows Mongoose to provide the complete networking stack - from Ethernet and TCP/IP to secure HTTPS, MQTT, and WebSocket communication.

NOTE: Mongoose core is not thread-safe. All mg_* API functions must be called from the same thread or RTOS task.

Summary

Mongoose uses a compact event-driven networking architecture designed for embedded systems.

The architecture revolves around an event manager, a polling event loop, and connection objects that represent active network connections.

Protocol handlers implement HTTP, WebSocket, MQTT and other protocols on top of the core event system.

This design allows developers to implement embedded web servers, device dashboards, APIs, and cloud connectivity with minimal complexity while maintaining high performance on resource-constrained devices.