Back to Blog
Embedded Systems

FreeRTOS on ESP32: Task Scheduling, Queues, and Resource Management for IoT

Bare-metal loops fall apart in complex IoT firmware. FreeRTOS gives you deterministic task scheduling, safe inter-task communication, and the resource management primitives that production firmware demands. Here is how to use them correctly on the ESP32.

February 28, 2024
14 min read
FreeRTOSESP32RTOSTask Scheduling

FreeRTOS on ESP32: Task Scheduling, Queues, and Resource Management for IoT

When IoT firmware grows beyond reading a sensor and blinking an LED, bare-metal super-loops start causing subtle, hard-to-reproduce bugs. WiFi reconnection blocks sensor sampling. MQTT publish blocks display updates. A slow JSON serialization pass causes you to miss a real-time control deadline. An RTOS eliminates these problems by giving each concern its own preemptible thread of execution.

The ESP-IDF ships FreeRTOS as its default task scheduler — and for good reason. After years of shipping production IoT firmware, FreeRTOS on ESP32 is our standard starting point for any project with more than two concurrent concerns.

Why RTOS Over Bare-Metal for IoT

A bare-metal super-loop executes tasks sequentially. If WiFi takes 2 seconds to reconnect, your sensor reading is 2 seconds late. With FreeRTOS, the WiFi reconnection runs in its own task at low priority; the high-priority sensor task preempts it every 100 ms regardless.

ESP32's dual-core architecture makes this even more powerful. FreeRTOS on ESP-IDF is a symmetric multiprocessing variant — tasks can be pinned to Core 0 (protocol CPU) or Core 1 (application CPU), giving you true parallelism.

Task Creation and Priority Design

Priorities in FreeRTOS are integers where higher numbers mean higher priority. ESP-IDF reserves priority 24 for the timer daemon; keep your tasks below that.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"

// Forward declarations void sensor_task(void *pvParameters); void mqtt_publish_task(void *pvParameters); void display_task(void *pvParameters);

// Shared queue handle — sensor → MQTT QueueHandle_t sensor_data_queue;

void app_main(void) { // Create queue: 10 items, each a sensor_reading_t struct sensor_data_queue = xQueueCreate(10, sizeof(sensor_reading_t)); configASSERT(sensor_data_queue != NULL);

// Sensor task: high priority, pinned to Core 1 (app CPU) xTaskCreatePinnedToCore( sensor_task, // Task function "sensor", // Task name (for debugging) 4096, // Stack size in bytes NULL, // Parameters 5, // Priority (5 = higher than MQTT) NULL, // Task handle (optional) 1 // Core 1 );

// MQTT publish: medium priority, Core 0 (protocol CPU) xTaskCreatePinnedToCore( mqtt_publish_task, "mqtt_pub", 8192, // MQTT needs more stack for TLS NULL, 3, NULL, 0 // Core 0 — alongside WiFi stack );

// Display: low priority, any core xTaskCreate(display_task, "display", 4096, NULL, 1, NULL); }

Key rule: assign priorities based on deadline urgency, not importance. A sensor reading with a 100 ms deadline outranks a display refresh with a 500 ms deadline.

Queues for Safe Inter-Task Communication

Queues are the primary mechanism for passing data between tasks without shared memory races. The sending task copies data into the queue; the receiving task copies it out. The queue is thread-safe by design.

typedef struct {
    float    temperature;
    float    humidity;
    uint32_t timestamp_ms;
} sensor_reading_t;

void sensor_task(void *pvParameters) { sensor_reading_t reading;

for (;;) { // Read sensor (blocking I2C call) if (sht31_read(&reading.temperature, &reading.humidity) == ESP_OK) { reading.timestamp_ms = xTaskGetTickCount() * portTICK_PERIOD_MS;

// Send to queue — block up to 10 ms if queue is full // Never use portMAX_DELAY in high-priority tasks (blocks scheduler) if (xQueueSend(sensor_data_queue, &reading, pdMS_TO_TICKS(10)) != pdTRUE) { ESP_LOGW("SENSOR", "Queue full — dropped reading"); } }

vTaskDelay(pdMS_TO_TICKS(100)); // Sample at 10 Hz } }

void mqtt_publish_task(void *pvParameters) { sensor_reading_t reading; char payload[128];

for (;;) { // Block indefinitely waiting for a new reading if (xQueueReceive(sensor_data_queue, &reading, portMAX_DELAY) == pdTRUE) { snprintf(payload, sizeof(payload), "{"temp":%.2f,"hum":%.2f,"ts":%lu}", reading.temperature, reading.humidity, reading.timestamp_ms); mqtt_publish("devices/sensor01/data", payload); } } }

Mutexes for Shared Resources

When two tasks share a resource (SPI bus, UART, a global configuration structure), use a mutex. Never access shared hardware directly from multiple tasks without one.

SemaphoreHandle_t spi_mutex;

void init_shared_spi(void) { spi_mutex = xSemaphoreCreateMutex(); configASSERT(spi_mutex != NULL); }

esp_err_t spi_write_protected(const uint8_t *data, size_t len) { // Acquire mutex — wait up to 100 ms if (xSemaphoreTake(spi_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { ESP_LOGE("SPI", "Failed to acquire SPI mutex"); return ESP_ERR_TIMEOUT; }

esp_err_t ret = spi_device_transmit(spi_handle, &transaction);

xSemaphoreGive(spi_mutex); // Always release — even on error return ret; }

Critical pitfall: Never call xSemaphoreTake from an ISR. Use xSemaphoreTakeFromISR and the corresponding BaseType_t pxHigherPriorityTaskWoken pattern.

Stack Overflow Detection

Stack overflows are the number-one silent killer in FreeRTOS firmware. Enable stack overflow checking in sdkconfig:

CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL=y

Then implement the hook:

void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    ESP_LOGE("FREERTOS", "Stack overflow in task: %s", pcTaskName);
    // Log to NVS before restarting so you can diagnose later
    nvs_log_crash("stack_overflow", pcTaskName);
    esp_restart();
}

Use uxTaskGetStackHighWaterMark(NULL) periodically in each task during development to measure actual peak stack usage, then size your stacks at 1.5× that value for production.

Common Pitfalls

Calling blocking functions from ISRs: Never call xQueueSend, vTaskDelay, or any FreeRTOS blocking API from an interrupt. Use the FromISR variants and yield if pxHigherPriorityTaskWoken is set.

Starvation of low-priority tasks: If your high-priority task never blocks, lower-priority tasks never run. Always call vTaskDelay or a blocking queue/semaphore operation in every task loop.

Heap fragmentation: Every xTaskCreate allocates stack from the heap. In long-running firmware, create all tasks at startup and never delete them. See our guide on [embedded firmware memory management](/embedded-firmware-memory-management) for deeper coverage.

portMAX_DELAY in network tasks: WiFi and MQTT calls can block indefinitely on connection loss. Always use a timeout and a reconnection state machine rather than blocking forever.

FreeRTOS on ESP32 is production-ready when used correctly. The patterns above have served us across dozens of deployments with multi-year uptime requirements.

[Contact Code Caracal](/contact) — we build production firmware for clients across 15+ countries.

Written by CodeCaracal Engineering

We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

More Articles

Business · 12 min read

IoT Device Compliance: FCC, CE, and Product Certification Guide for Hardware Startups

Business · 11 min read

What to Look for When Hiring an IoT Development Partner: 8 Critical Criteria

Business · 11 min read

IoT MVP to Production: Realistic Timeline and Budget for Hardware Startups

Business · 11 min read

IoT Development Agency vs Building In-House: A Decision Framework for Founders

IoT Dashboard · 13 min read

Next.js IoT Analytics Dashboard: From Sensor Data to Production App

Business · 11 min read

How Much Does It Cost to Build an IoT Product in 2024? A Realistic Breakdown

IoT Dashboard · 11 min read

IoT Dashboard UX: Design Principles for Industrial Monitoring Interfaces

IoT Dashboard · 12 min read

Node.js WebSocket Server: The Real-Time Backend for IoT Dashboards

Cloud & DevOps · 12 min read

Containerizing IoT Backend Services with Docker: From Dev to Production

IoT Dashboard · 14 min read

Grafana + InfluxDB IoT Monitoring: Complete Production Setup Guide

IoT Dashboard · 12 min read

Building Real-Time IoT Dashboards with React and Recharts

Cloud & DevOps · 13 min read

CI/CD for Embedded Firmware: Automated Build, Test, and OTA Release Pipeline

Mobile Development · 12 min read

Flutter Offline-First IoT Apps: Hive + Sync Architecture That Works in the Field

Cloud & DevOps · 14 min read

Terraform for IoT Infrastructure: Provisioning AWS IoT Core, Lambda, and InfluxDB as Code

Mobile Development · 10 min read

Flutter IoT Alerts: Firebase Push Notifications for Device Events

Cloud & DevOps · 12 min read

Deploying IoT Backends on AWS: ECS Fargate vs Lambda vs EC2 Decision Guide

Mobile Development · 11 min read

Flutter + MQTT: Building Production IoT Mobile Apps That Scale

Mobile Development · 13 min read

Flutter BLE: Building a Bluetooth IoT Controller App from Scratch

Cloud & DevOps · 13 min read

AWS IoT Core vs Azure IoT Hub vs Google Cloud IoT: 2024 Honest Comparison

IoT Engineering · 13 min read

Kafka vs RabbitMQ for IoT: Choosing the Right Message Queue for High-Volume Telemetry

IoT Engineering · 14 min read

IoT System Testing: Unit, Integration, Hardware-in-the-Loop, and End-to-End

IoT Engineering · 14 min read

Predictive Maintenance with IoT Sensor Data: From Threshold to Machine Learning

Embedded Systems · 14 min read

IoT Bootloader Design: Secure Boot, A/B Partitions, and Reliable OTA Recovery

IoT Engineering · 14 min read

Multi-Tenant IoT Platform Architecture: Isolation, Scaling, and Data Partitioning

Embedded Systems · 14 min read

Memory Management in Embedded Firmware: Avoiding Heap Fragmentation and Stack Overflows

IoT Engineering · 13 min read

IoT Cost Optimization: How We Cut AWS IoT Bills by 60% Without Sacrificing Reliability

IoT Engineering · 12 min read

Edge Computing in IoT: When to Process On-Device vs In the Cloud

IoT Engineering · 13 min read

Digital Twins for IoT: Building a Virtual Mirror of Your Physical Devices

Embedded Systems · 14 min read

ESP32 Deep Sleep Mastery: Cutting Power Consumption from 240mA to 10µA

IoT Engineering · 10 min read

MQTT QoS 0, 1, and 2 Explained: Choosing the Right Level for IoT

IoT Engineering · 14 min read

IoT Monitoring and Observability: Metrics, Logs, and Distributed Tracing

Embedded Systems · 14 min read

Debugging Embedded Firmware: JTAG, GDB, Logic Analyzers, and Serial Tracing

IoT Engineering · 12 min read

WebSocket vs MQTT vs Server-Sent Events: Real-Time IoT Protocol Deep Dive

Embedded Systems · 13 min read

STM32 HAL vs Low-Level Drivers: When the Abstraction Costs You Too Much

IoT Engineering · 13 min read

IoT Data Pipeline: From Raw Sensor Reading to Live Dashboard in Under 100ms

IoT Engineering · 13 min read

Zero-Touch IoT Device Provisioning: Scaling from 10 to 100,000 Devices

Embedded Systems · 13 min read

UART vs SPI vs I2C: Choosing the Right Protocol for Sensor Integration

IoT Engineering · 12 min read

Real-Time IoT Alerting: From Simple Thresholds to ML Anomaly Detection

Embedded Systems · 12 min read

ESP32 Partition Table: Designing Flash Layout for Production Firmware

IoT Engineering · 12 min read

IoT Architecture Patterns: Hub-and-Spoke, Mesh, and Edge-Cloud Hybrid

Embedded Systems · 13 min read

IoT Battery Life Optimization: Engineering Devices That Last Years on a Single Charge

IoT Engineering · 13 min read

Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

Security · 14 min read

Zero-Trust Security for Embedded IoT: Why Your Devices Are Probably Vulnerable

IoT Engineering · 12 min read

Building a Production IoT Gateway with Raspberry Pi and Node.js

Embedded Systems · 13 min read

ESP32 vs STM32: Choosing the Right Microcontroller for Your IoT Project

Mobile Development · 10 min read

Flutter + WebSocket: Building Real-Time IoT Dashboards That Don't Stutter

IoT Engineering · 13 min read

IoT Fleet Management at Scale: AWS IoT Core Device Registry and Provisioning

IoT Engineering · 11 min read

MQTT vs HTTP for IoT: Which Protocol Wins in Production?

IoT Engineering · 12 min read

ESP32 → MQTT → AWS IoT Core: The Production-Grade Architecture Guide

Let's Build Together

Got an IoT challenge?
We've shipped it.

Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

No upfront commitment99.9% uptime SLANDA on requestFixed-price options