In this tutorial, we’ll introduce the basic concepts of FreeRTOS and show you how to use it with the ESP32 and the Arduino IDE. You’ll learn how to create single and multiple tasks, suspend and resume tasks, run code on the ESP32’s two cores, and calculate the appropriate stack size needed (memory) for each task.
FreeRTOS is a real-time operating system that allows the ESP32 to manage and run multiple tasks simultaneously in a smooth and efficient way. It’s built into the ESP32 and fully integrated with both the Arduino core and the Espressif IoT Development Framework (IDF).

In this tutorial, we’ll cover the following topics:
- What is FreeRTOS
- 1) Creating Tasks
- 2) Suspend and Resume Tasks
- 3) Creating and Running Multiple Tasks
- 4) Creating and Running Multiple Tasks (Dual Core)
- 5) Tasks Memory Usage
Prerequisites
This tutorial focuses on programming the ESP32 using the Arduino core. Before proceeding, you should have the ESP32 Arduino core installed in your Arduino IDE. Follow the next tutorial to install the ESP32 on the Arduino IDE, if you haven’t already.
What is FreeRTOS?
FreeRTOS is a lightweight, open-source real-time operating system (RTOS). It provides a framework for running multiple tasks concurrently, each with its own priority and execution schedule. Instead of running code line by line, FreeRTOS lets you create independent tasks that the ESP32 can switch between quickly based on the task priority.

It also provides tools like queues and semaphores so tasks can seamlessly communicate with each other.
This makes your code more organized and responsive, especially when your ESP32 is handling several tasks at the same time, like reading sensors, handling HTTP requests, and displaying information on a screen, while listening to interrupts.
Why is FreeRTOS useful with the ESP32?
The FreeRTOS real-time operating system is built into the ESP32 and integrated into the Espressif IDF and the Arduino core. It supports:
- Task Management: create, suspend, resume, or delete tasks (covered in this tutorial).
- Scheduling: allows you to give priority to your tasks, so they run in a specific order (covered in this tutorial).
- Inter-Task Communication: using things like queues, semaphores, and mutexes, we can ensure seamless communication between tasks without crashing the ESP32.
- Dual-Core Support: it allows you to run your tasks on either core 0 or core 1 of the ESP32 (also covered in this tutorial, but for a more in-depth guide about using dual-core with the ESP32, check this guide: How to use ESP32 Dual Core with Arduino IDE).
So, in summary…
FreeRTOS is useful with the ESP32 because it enables multitasking, allowing multiple tasks like sensor reading, Wi-Fi, and display updates to run without blocking each other.
It also allows you to take advantage of the ESP32 dual-core processor by letting you assign tasks to specific cores for better performance.
Additionally, with priority-based scheduling, time-critical tasks can run immediately, making it ideal for real-time applications when you need to react quickly to external events detected by the ESP32 GPIOs.
FreeRTOS Basic Concepts
Before diving into the practical examples, let’s cover some basic concepts related to FreeRTOS:
- Tasks: tasks are independent functions running concurrently, each with its own stack (memory usage allocated) and priority. Tasks can be in states like Running, Ready, Blocked, or Suspended.
- Scheduler: the scheduler decides which tasks to run based on their priorities. This is a preemptive scheduler, which means it can interrupt a lower-priority task at any time to run a higher-priority one, ensuring that critical tasks are executed as soon as they’re ready.
- Priorities: higher numbers indicate higher priority (for example: 1 = low, 5 = high).
1) Creating Tasks
This is the simplest example in which we’ll show you how to create a FreeRTOS task to blink an LED every second. We’ll connect the LED to GPIO 2. Instead, you can skip the LED and check the results on the ESP32 built-in LED.
Parts Required
For this example, you need the following parts:
- ESP32 Board – read Best ESP32 Development Boards
- LED
- 220k Ohm resistor (or similar values)
- Breadboard
- Jumper wires
You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!
Wiring the Circuit
Wire an LED to GPIO 2 as shown in the following schematic diagram.

ESP32: Creating FreeRTOS Tasks – Arduino Code
The following code creates a task that will blink an LED connected to GPIO 2 every second.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define LED_PIN 2
// Declare task handle
TaskHandle_t BlinkTaskHandle = NULL;
void BlinkTask(void *parameter) {
for (;;) { // Infinite loop
digitalWrite(LED_PIN, HIGH);
Serial.println("BlinkTask: LED ON");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
digitalWrite(LED_PIN, LOW);
Serial.println("BlinkTask: LED OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.print("BlinkTask running on core ");
Serial.println(xPortGetCoreID());
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
xTaskCreatePinnedToCore(
BlinkTask, // Task function
"BlinkTask", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&BlinkTaskHandle, // Task handle
1 // Core 1
);
}
void loop() {
// Empty because FreeRTOS scheduler runs the task
}
How Does the Code Work?
We start by defining the GPIO that will be connected to the LED.
#define LED_PIN 2
Task Handle
Then, declare the task handle. A TaskHandle_t is a variable that points to a FreeRTOS task, letting you control it, like resuming it, stopping it, or delete it. In this example, we won’t need the task handle, but we’re creating it nonetheless to show you how it’s done.
TaskHandle_t BlinkTaskHandle = NULL;
Task Function
Then, we create a task. A task is nothing more than a function that executes whatever commands you want. Here’s the BlinkTask function used in this example.
void BlinkTask(void *parameter) {
for (;;) { // Infinite loop
digitalWrite(LED_PIN, HIGH);
Serial.println("BlinkTask: LED ON");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
digitalWrite(LED_PIN, LOW);
Serial.println("BlinkTask: LED OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.print("BlinkTask running on core ");
Serial.println(xPortGetCoreID());
}
}
This function is a FreeRTOS task, which is a special kind of function that runs independently under the FreeRTOS scheduler, allowing multitasking on the ESP32.
FreeRTOS tasks must return void and must accept a single argument, which can be used to pass data to the function (not used in our case).
void BlinkTask(void *parameter) {
The for(;;) creates an infinite loop to ensure the task runs indefinitely until explicitly stopped. This is similar to the loop() function used in Arduino code.
for (;;) { // Infinite loop
Then, we use the digitalWrite() function to turn the LED on and off. Notice that instead of using the typical delay() function, we use vTaskDelay().
digitalWrite(LED_PIN, HIGH);
Serial.println("BlinkTask: LED ON");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
digitalWrite(LED_PIN, LOW);
Serial.println("BlinkTask: LED OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelay()
vTaskDelay() is a FreeRTOS function that pauses a task for a specified number of ticks, allowing other tasks to run during that time. It doesn’t block your code like delay(). The vTaskDelay() function accepts ticks. On the ESP32, each tick is typically 1ms (defined by portTICK_PERIOD_MS), so vTaskDelay(1000 / portTICK_PERIOD_MS) pauses the task for 1000ms (1 second).
vTaskDelay(1000 / portTICK_PERIOD_MS);
Get Core IDE
For demonstration purposes, we also print in which core the task is running. We can get that information by calling the xPortGetCoreID() function.
Serial.print("BlinkTask running on core ");
Serial.println(xPortGetCoreID());
setup()
In the setup(), we initialize the Serial Monitor and set the LED as an output.
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
Create a Task
Now, to actually create a FreeRTOS task and assign it to a specific core, we need to use the xTaskCreatePinnedToCore() function. This function also specifies the task function, name, stack size, parameters, priority, and task handle.
xTaskCreatePinnedToCore(
BlinkTask, // Task function
"BlinkTask", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&BlinkTaskHandle, // Task handle
1 // Core 1
);
The task function is BlinkTask that we defined earlier. We can also give a name to the task. In this case, “BlinkTask”.
BlinkTask, // Task function
"BlinkTask", // Task name
We set the task stack size to 10000 bytes. The task stack size is the amount of memory allocated for the task to store its variables, function calls, and temporary data while it runs, ensuring it has enough space to operate without crashing the ESP32. It is defined in bytes. In a later example, we’ll see how to get the stack size of a task.
10000, // Stack size (bytes)
In this case, our task doesn’t have any parameters, so we set that parameter to NULL.
NULL, // Parameters
We give the task priority 1. The higher the number, the higher the priority. In this case, it doesn’t matter much because we only have one task.
1, // Priority
We also define the task handle for the task that we created at the beginning of the code.
&BlinkTaskHandle, // Task handle
And finally, we define in which core we want to run the task. The ESP32 has two cores, designated core 0 and core 1. In this example, we’re using core 1.
1 // Core 1
loop()
The loop() is empty because the FreeRTOS scheduler will run the task. However, it is possible to add code to the loop() to run any other commands you want.
void loop() {
// Empty because FreeRTOS scheduler runs the task
}
Demonstration
Upload the code to your ESP32 board. After uploading, open the Serial Monitor at a baud rate of 115200. You should get a similar result.

At the same time, the LED should be blinking every second.


2) Suspend and Resume Tasks
In this example, you’ll learn how to suspend and resume a FreeRTOS task on the ESP32. We’ll create a task that blinks an LED, and use a pushbutton to control it. When the button is pressed, the task will be suspended to stop the blinking, and pressing it again will resume the task.
Parts Required
For this example, you need the following parts:
- ESP32 Board – read Best ESP32 Development Boards
- LED
- 220k Ohm resistor (or similar values)
- Pushubutton
- Breadboard
- Jumper wires
Wiring the Circuit
Add a pushbutton to your previous circuit. We’re connecting the pushbutton to GPIO 23, but you can use any other suitable GPIO.

Recommended reading: ESP32 Pinout Reference: Which GPIO pins should you use?
ESP32: Suspend and Resume FreeRTOS Tasks – Arduino Code
The following code listens to the press of a pushbutton to either suspend or resume the blinking FreeRTOS task.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define LED1_PIN 2
#define BUTTON_PIN 23
// Task handle
TaskHandle_t BlinkTaskHandle = NULL;
// Volatile variables for ISR
volatile bool taskSuspended = false;
volatile uint32_t lastInterruptTime = 0;
const uint32_t debounceDelay = 100; // debounce period
void IRAM_ATTR buttonISR() {
// Debounce
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime < debounceDelay) {
return;
}
lastInterruptTime = currentTime;
// Toggle task state
taskSuspended = !taskSuspended;
if (taskSuspended) {
vTaskSuspend(BlinkTaskHandle);
Serial.println("BlinkTask Suspended");
} else {
vTaskResume(BlinkTaskHandle);
Serial.println("BlinkTask Resumed");
}
}
void BlinkTask(void *parameter) {
for (;;) { // Infinite loop
digitalWrite(LED1_PIN, HIGH);
Serial.println("BlinkTask: LED ON");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 1000ms
digitalWrite(LED1_PIN, LOW);
Serial.println("BlinkTask: LED OFF");
Serial.print("BlinkTask running on core ");
Serial.println(xPortGetCoreID());
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
// Initialize pins
pinMode(LED1_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // Internal pull-up resistor
// Attach interrupt to button
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
// Create task
xTaskCreatePinnedToCore(
BlinkTask, // Task function
"BlinkTask", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&BlinkTaskHandle, // Task handle
1 // Core 1
);
}
void loop() {
// Empty because FreeRTOS scheduler runs the task
}
How Does the Code Work?
We already covered how to create tasks in the previous example. So, we’ll just cover the relevant parts for this section.
We define the GPIOs for the LED and the pushbutton.
#define LED1_PIN 2
#define BUTTON_PIN 23
We create volatile variables that will be used in the ISR (interrupt service routine for the pushbutton). The taskSuspended variable is used to determine whether the task is suspended or not, and the lastInterruptTime and debounceDelay are needed to debounce the pushbutton.
// Volatile variables for ISR
volatile bool taskSuspended = false;
volatile uint32_t lastInterruptTime = 0;
const uint32_t debounceDelay = 100; // debounce period
To detect pushbutton presses, we’re using interrupts. When we use interrupts, we need to define an interrupt service routine (a function that runs on the ESP32 RAM). In this case, we create the buttonISR() function. We must add IRAM_ATTR to the function definition to run it on RAM.
void IRAM_ATTR buttonISR() {
Recommended reading: ESP32 with PIR Motion Sensor using Interrupts and Timers.
First, we debounce the pushbutton, and if a button press is detected, we toggle the state of the taskSuspended flag variable.
// Debounce
uint32_t currentTime = millis();
if (currentTime - lastInterruptTime < debounceDelay) {
return;
}
lastInterruptTime = currentTime;
// Toggle task state
taskSuspended = !taskSuspended;
Suspend and Resume Tasks
Then, if the taskSuspended variable is true, we call the vTaskSuspend() function and pass the task handle as an argument. Here, you can see one of the uses of the Task Handle. It is a way to refer to the task to control it.
if (taskSuspended) {
vTaskSuspend(BlinkTaskHandle);
If the taskSuspended variable is false, we call the vTaskResume() function to resume the execution of the task.
} else {
vTaskResume(BlinkTaskHandle);
setup()
In the setup(), we must declare our pushbutton as an interrupt as follows:
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
The rest of the code remains similar to the previous example.
Demonstration
Upload the code to your ESP32 board.
The LED will start blinking. If you press the pushbutton, the LED will stop blinking. Press again and the LED will start blinking again.

You can check how it works by watching the following video.
At the same time, you should get all the information on the Serial Monitor.

3) Creating and Running Multiple Tasks
In this section, you’ll learn how to create and run multiple FreeRTOS tasks simultaneously. As an example, we’ll create two tasks to blink two different LEDs.
Parts Required
For this example, you need the following parts:
- ESP32 Board – read Best ESP32 Development Boards
- 2x LED
- 2x 220k Ohm resistor (or similar values)
- Pushbutton
- Breadboard
- Jumper wires
Wiring the Circuit
We’ll wire two LEDs to the ESP32. We’ll use GPIOs 2 and 4. You can use any other suitable GPIOs as long as you modify the code accordingly.

ESP32: Creating and Running Multiple Tasks – Arduino Code
The following code creates two separate FreeRTOS tasks, each blinking an LED at a different rate. With FreeRTOS, it’s easy to run both tasks independently without blocking each other. This approach is much cleaner and more efficient than using delays or complex timing logic in traditional Arduino code.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define LED1_PIN 2
#define LED2_PIN 4
TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;
void Task1(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
for (;;) {
digitalWrite(LED1_PIN, HIGH);
Serial.println("Task1: LED1 ON");
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
Serial.println("Task1: LED1 OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void Task2(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println("Task2: LED2 ON");
vTaskDelay(333 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println("Task2: LED2 OFF");
vTaskDelay(333 / portTICK_PERIOD_MS);
}
}
void setup() {
Serial.begin(115200);
xTaskCreatePinnedToCore(
Task1, // Task function
"Task1", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task1Handle, // Task handle
1 // Core 1
);
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task2Handle, // Task handle
1 // Core 1
);
}
void loop() {
// Empty because FreeRTOS scheduler runs the task
}
How Does the Code Work?
Creating and running multiple tasks is as simple as creating and running a single task.
First, you need to define your tasks. In our case, we have two different tasks to blink two different LEDs at different rates. We call them Task1 and Task2.
void Task1(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
for (;;) {
digitalWrite(LED1_PIN, HIGH);
Serial.println("Task1: LED1 ON");
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
Serial.println("Task1: LED1 OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void Task2(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println("Task2: LED2 ON");
vTaskDelay(333 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println("Task2: LED2 OFF");
vTaskDelay(333 / portTICK_PERIOD_MS);
}
}
Then, in the setup(), we just need to create the tasks using the xTaskCreatePinnedToCore. In this example, we’re running both tasks on core 1 of the ESP32 and both have the same priority.
xTaskCreatePinnedToCore(
Task1, // Task function
"Task1", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task1Handle, // Task handle
1 // Core 1
);
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task2Handle, // Task handle
1 // Core 1
);
Demonstration
Upload the code to your board. After uploading, press the ESP32 RST button so that it starts running the code. You’ll have two different LEDs blinking at different rates.

In the following video, you can better understand how it works.
As you can see, it is super simple to achieve this by creating FreeRTOS tasks instead of using delays or other complex timing calculations.
4) Creating and Running Multiple Tasks On Different Cores (ESP32 Dual-Core)
Most ESP32 models come with two cores, designated core 0 and core 1. By default, when we run code on the Arduino IDE, the code runs on core 1. In this section, we’ll show you how to run different tasks on different ESP32 cores.
Wiring the Circuit
Maintain the circuit from the previous example with two LEDs.
Tasks on Multiple Cores
The following code is similar to the previous example, but each task is running on a different core. Additionally, we also add a line to each task to double-check in which core it is running.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define LED1_PIN 2
#define LED2_PIN 4
TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;
void Task1(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
for (;;) {
digitalWrite(LED1_PIN, HIGH);
Serial.println("Task1: LED1 ON");
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
Serial.println("Task1: LED1 OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.print("Task 1 running on core ");
Serial.println(xPortGetCoreID());
}
}
void Task2(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println("Task2: LED2 ON");
vTaskDelay(333 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println("Task2: LED2 OFF");
vTaskDelay(333 / portTICK_PERIOD_MS);
Serial.print("Task 2 running on core ");
Serial.println(xPortGetCoreID());
}
}
void setup() {
Serial.begin(115200);
xTaskCreatePinnedToCore(
Task1, // Task function
"Task1", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task1Handle, // Task handle
1 // Core 1
);
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task2Handle, // Task handle
0 // Core 0
);
}
void loop() {
// Empty because FreeRTOS scheduler runs the task
}
How Does the Code Work?
The only difference in this example is that we define Task2 to run on core 0 as shown below.
xTaskCreatePinnedToCore(
Task2, // Task function
"Task2", // Task name
10000, // Stack size (bytes)
NULL, // Parameters
1, // Priority
&Task2Handle, // Task handle
0 // Core 0
);
Demonstration
If you upload the code to your ESP32, the result will be the same as the previous example. Two LEDs are blinking at different rates.

In the Serial Monitor, you can actually check that each task is running in a different core.

5) Tasks Memory Usage
In this section, we’ll cover how to measure stack and heap usage for your tasks so that you can optimize memory allocation.
Stack and heap are two types of memory used by FreeRTOS tasks on the ESP32, each serving a unique role in managing a program’s memory needs.
What exactly is the Stack Usage? The stack is a dedicated memory area for each FreeRTOS task, used to store temporary data like local variables, function call information, and task state during execution. Each task has its own stack, allocated when the task is created. You’ve seen that in previous examples, we’re allocating a 10000-byte stack to each task.
There is a function that you can call inside your task to determine the stack usage: the uxTaskGetStackHighWaterMark() function. That function determines the allocated stack size that is not being used.
What is the Heap Usage? The heap is a shared memory pool in the ESP32’s SRAM, used for dynamic memory allocation, including task stacks, buffers, and other runtime data allocated by FreeRTOS or the Arduino core. We can call the xPortGetFreeHeapSize() function in our code to determine the free heap.
Task Stack Size and Free Heap – Code
The following code is similar to the one in previous projects, with two functions to blink two different LEDs. We determine the free stack and free heap.
Upload the following code to your board.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-arduino-tasks/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
#define LED1_PIN 2
#define LED2_PIN 4
TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;
void Task1(void *parameter) {
pinMode(LED1_PIN, OUTPUT);
for (;;) {
digitalWrite(LED1_PIN, HIGH);
Serial.println("Task1: LED1 ON");
vTaskDelay(1000 / portTICK_PERIOD_MS);
digitalWrite(LED1_PIN, LOW);
Serial.println("Task1: LED1 OFF");
vTaskDelay(1000 / portTICK_PERIOD_MS);
Serial.printf("Task1 Stack Free: %u bytes\n", uxTaskGetStackHighWaterMark(NULL));
}
}
void Task2(void *parameter) {
pinMode(LED2_PIN, OUTPUT);
for (;;) {
digitalWrite(LED2_PIN, HIGH);
Serial.println("Task2: LED2 ON");
vTaskDelay(333 / portTICK_PERIOD_MS);
digitalWrite(LED2_PIN, LOW);
Serial.println("Task2: LED2 OFF");
vTaskDelay(333 / portTICK_PERIOD_MS);
Serial.printf("Task2 Stack Free: %u bytes\n", uxTaskGetStackHighWaterMark(NULL));
}
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.printf("Starting FreeRTOS: Memory Usage\nInitial Free Heap: %u bytes\n", xPortGetFreeHeapSize());
xTaskCreatePinnedToCore(
Task1,
"Task1",
10000,
NULL,
1,
&Task1Handle,
1
);
xTaskCreatePinnedToCore(
Task2,
"Task2",
10000,
NULL,
1,
&Task2Handle,
1
);
}
void loop() {
static uint32_t lastCheck = 0;
if (millis() - lastCheck > 5000) {
Serial.printf("Free Heap: %u bytes\n", xPortGetFreeHeapSize());
lastCheck = millis();
}
}
Demonstration
After uploading the code to your board, you should get something similar on your Serial Monitor.

The uxTaskGetStackHighWaterMark reports 8556 bytes free for Task1 and 8552 bytes free for Task2, meaning each task uses 1444–1448 bytes of its 10000-byte stack at peak. So, we can greatly reduce the allocated stack size to each task.
A good stack size should cover the task’s peak usage (1444 bytes) plus a safety margin of 500–1000 bytes to handle unexpected increases
In the case of free heap, in my case, the xPortGetFreeHeapSize() reports 247616 bytes free (not shown in the screenshot), indicating the remaining heap after allocating stacks (20000 bytes for two tasks) and other system resources.
Wrapping Up
This tutorial was a detailed introduction to FreeRTOS with the ESP32. You learned how to create single and multiple tasks, assign a core to each task, suspend and resume tasks, and even calculate the task stack.
Using FreeRTOS with the ESP32 is a great choice because it allows you to perform multiple tasks simultaneously in a simple way, with priorities to run the most critical tasks first.
In future tutorials, we’ll cover communication between tasks using semaphores and queues.
We hope you’ve found this guide useful.
Learn more about the ESP32 with our resources:
Thanks for reading.