50% OFF

ESP32-IDF Workshop

Blog/Embedded Systems

ESP32 Dual-Core Task Pinning with FreeRTOS: A Production Guide

Most ESP32 tutorials ignore dual-core pinning entirely. Here's how production firmware uses both cores — with real task allocation patterns from the field.

| Intermediate
Rajath Kumar
Rajath KumarEdge AI Engineer & Founder, Analog Data
2026-06-26·9 min read
ESP32 Dual-Core Task Pinning with FreeRTOS: A Production Guide

Most ESP32 tutorials show you how to create a FreeRTOS task. Almost none of them tell you which core to run it on — and that decision alone can make the difference between reliable firmware and random watchdog resets at 3am.

This is the guide I wish existed when I was debugging a production IoT gateway that kept crashing under load.


What You'll Learn

  • How ESP32's two cores are actually used by ESP-IDF internals
  • When to pin tasks vs. let the scheduler decide
  • Real task allocation patterns from production firmware
  • Common mistakes that cause core starvation and WDT resets

The Two Cores: What ESP-IDF Actually Does With Them

ESP32 has two Xtensa LX6 cores:

CoreNameDefault Role in ESP-IDF
Core 0PRO_CPUWiFi, BT stack, system tasks
Core 1APP_CPUYour application code

Common mistake: Most developers create all their tasks without pinning them. The FreeRTOS scheduler then distributes them across both cores — which sounds fine until your WiFi stack and a sensor polling task start competing for Core 0 at the same time.


xTaskCreate vs xTaskCreatePinnedToCore

This is the core API decision:

c
1// ❌ Unpinned — scheduler picks the core
2xTaskCreate(
3    sensor_task,        // Task function
4    "sensor_task",      // Task name
5    4096,               // Stack size in bytes
6    NULL,               // Parameters
7    5,                  // Priority
8    &sensor_task_handle // Task handle
9);
10
11// ✅ Pinned to APP_CPU — you control the core
12xTaskCreatePinnedToCore(
13    sensor_task,        // Task function
14    "sensor_task",      // Task name
15    4096,               // Stack size in bytes
16    NULL,               // Parameters
17    5,                  // Priority
18    &sensor_task_handle,// Task handle
19    1                   // Core ID: 0 = PRO_CPU, 1 = APP_CPU
20);

Rule of thumb: In production firmware, always use xTaskCreatePinnedToCore. Never leave core assignment to chance.


Production Task Allocation Pattern

Here's the actual pattern used across IoT gateway firmware:

c
1// main.c — Production task allocation
2#include "freertos/FreeRTOS.h"
3#include "freertos/task.h"
4#include "esp_log.h"
5
6static const char *TAG = "MAIN";
7
8TaskHandle_t sensor_task_handle    = NULL;
9TaskHandle_t mqtt_task_handle      = NULL;
10TaskHandle_t inference_task_handle = NULL;
11TaskHandle_t display_task_handle   = NULL;
12
13#define STACK_SENSOR    4096
14#define STACK_MQTT      8192
15#define STACK_INFERENCE 16384
16#define STACK_DISPLAY   4096
17
18#define PRIO_SENSOR     6
19#define PRIO_MQTT       5
20#define PRIO_INFERENCE  4
21#define PRIO_DISPLAY    3
22
23void app_main(void) {
24    ESP_LOGI(TAG, "Starting task allocation...");
25
26    // Core 0 (PRO_CPU) — only tasks that NEED WiFi proximity
27    xTaskCreatePinnedToCore(mqtt_publish_task, "mqtt_task",
28        STACK_MQTT, NULL, PRIO_MQTT, &mqtt_task_handle, 0);
29
30    // Core 1 (APP_CPU) — all application workload
31    xTaskCreatePinnedToCore(sensor_polling_task, "sensor_task",
32        STACK_SENSOR, NULL, PRIO_SENSOR, &sensor_task_handle, 1);
33
34    xTaskCreatePinnedToCore(edge_inference_task, "inference_task",
35        STACK_INFERENCE, NULL, PRIO_INFERENCE, &inference_task_handle, 1);
36
37    xTaskCreatePinnedToCore(display_update_task, "display_task",
38        STACK_DISPLAY, NULL, PRIO_DISPLAY, &display_task_handle, 1);
39
40    ESP_LOGI(TAG, "All tasks pinned and running.");
41}

The Stack Size Problem Nobody Talks About

This is where most production crashes come from — not task pinning itself, but undersized stacks.

c
1// Monitor actual stack usage at runtime
2void monitor_task(void *pvParameters) {
3    while (1) {
4        ESP_LOGI("STACK", "sensor_task  watermark: %d bytes free",
5            uxTaskGetStackHighWaterMark(sensor_task_handle));
6        ESP_LOGI("STACK", "mqtt_task    watermark: %d bytes free",
7            uxTaskGetStackHighWaterMark(mqtt_task_handle));
8        ESP_LOGI("STACK", "inference    watermark: %d bytes free",
9            uxTaskGetStackHighWaterMark(inference_task_handle));
10
11        vTaskDelay(pdMS_TO_TICKS(5000));
12    }
13}

Production rule: Run your firmware under full load and log stack watermarks for 24 hours. If headroom drops below 512 bytes — double your stack allocation immediately.


Real-World Task Map: Industrial IoT Gateway

TaskCorePriorityStackNotes
modbus_poll184096RS485 sensor polling
edge_inference1616384Anomaly detection model
data_buffer174096Ring buffer management
mqtt_publish058192TLS MQTT to AWS IoT
ota_monitor034096OTA update checks
display_ui124096OLED status display
watchdog_feed192048System health monitor

This setup ran for 6 months on 40 deployed units without a single unexpected reboot.


Key Takeaways

  1. Always pin tasks using xTaskCreatePinnedToCore — never leave it to the scheduler in production
  2. Keep Core 0 light — WiFi and BT stacks live there
  3. Run heavy compute on Core 1 — ML inference, sensor processing, display updates
  4. Monitor stack watermarks during development — crashes in production are almost always stack overflows
  5. Sensor data collection gets highest priority — you can retry MQTT, you can't recover lost sensor readings
Share
Live Workshop

Go from Arduino to Production Firmware

The ESP32-IDF Workshop covers ESP-IDF from scratch — tasks, queues, OTA, Wifi management, and deploying firmware that doesn't break at 3am.

Join the Workshop →

Frequently Asked Questions

Quick answers to common questions

Rajath Kumar

Written by

Rajath Kumar

Edge AI Engineer & Founder, Analog Data

I build things that run on chips and the software that talks to them. ESP32, STM32, FreeRTOS, FastAPI, TinyML — from bare-metal firmware to cloud backends to on-device inference. Based in Bengaluru. Founder of Analog Data.

More in Embedded Systems