In this guide, you’ll learn how to use FreeRTOS queues for safe and efficient communication between tasks on the ESP32, using the Arduino IDE. Queues allow you to exchange data between tasks in a safe way. We’ll cover the basic principles of how queues work and walk you through three practical examples to show how to pass data between tasks.

New to FreeRTOS? Start with this tutorial: ESP32 with FreeRTOS (Arduino IDE)—Getting Started Guide: Creating Tasks.
Table of Contents
In this tutorial, we’ll cover the following subjects:
- FreeRTOS Communication Between Tasks
- What are FreeRTOS Queues?
- FreeRTOS Queue Basics
- Example 1: Using Queues to Control LED Brightness
- Example 2: FreeRTOS Queues – Sharing Data Between Three Tasks
- Example 3: Display Sensor Data on a Screen (Sharing a Data Structure)
FreeRTOS Communication Between Tasks
In FreeRTOS there are different ways for inter-task communication and synchronization of data between tasks, suited for different purposes.
- Queues: allow tasks to send and receive data, like sensor readings, button states, or other data, as we’ll see in the examples covered in this tutorial. Here we’ll focus on FreeRTOS queues.
- Semaphores are like signaling tools in FreeRTOS used to coordinate tasks. They are often used to indicate that an event has occurred or a resource is ready. They don’t carry data, just a count (or flag) to trigger actions after something has happened. We’ll cover this in a future tutorial.
- Mutexes (mutual exclusions) are used to protect shared resources by allowing only one task to access them at a time. This prevents conflicts in concurrent operations that need to access the same data. Unlike queues, mutexes focus on resource access rather than data transfer. We’ll cover this in a future tutorial.
What are FreeRTOS Queues?
FreeRTOS queues are thread-safe data structures that allow tasks to send and receive data for synchronized communication in a multitasking environment.
They ensure that data is passed safely between tasks without conflicts or data loss.
Queues are especially useful when one task needs to send data and another task needs to handle it later, like a sensor task sending readings to a logging task or to be processed to display on a screen. This way, each task can run at its own speed without interfering with the other.
FreeRTOS Queue Basics
Here are some basic concepts about creating and handling queues on your Arduino IDE code for the ESP32:
Creating a Queue
Use the xQueueCreate(size, item_size) to create a task—size corresponds to the number of items that can be on the queue and item_size is the bytes size of heap allocated for each item on the queue.
Sending Data to a Queue
To send data to a queue use the xQueueSend() to add data to the queue. Or use xQueueSendFromISR() if sending the data from an ISR (interrupt service routine).
Receive Data from a Queue
The xQueueReceive() function reads data from the queue, if available.
Example 1: Using Queues to Control LED Brightness

In this example, we’ll create two tasks. One task will send data to a queue. The other task will read data from the queue.
We’ll control the LED brightness using a potentiometer. This is a basic example, you’ve probably seen before, to learn about analog reading and PWM. In this case, we’ll use that basic example as a starting point to explain queues.
We’ll create two tasks with the following names:
- potTask: reads the value from the potentiometer and sends it to a queue.
- LEDBrightnessTask: checks if there are values to receive from the queue and adjusts the LED brightness accordingly.
Parts Required
For this project, you need the following parts:
- ESP32 Board of your choice – read Best ESP32 Development Boards
- LED
- 220 Ohm resistor
- Potentiometer
- 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
For this example, wire an LED and a potentiometer to your ESP32 board. We’re connecting the LED to GPIO 2 and the potentiometer to GPIO 15.

Code – Control LED Brightness (Queues Basic Example)
The following code creates two tasks. One reads the values from the potentiometer and sends them to a queue. The other task reads the values from the queue and controls the LED brightness accordingly.
You can copy the following code to the Arduino IDE and upload it to your board.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-queues-inter-task-arduino/
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 POT_PIN 15
#define LED_PIN 2
#define PWM_FREQ 5000
#define PWM_RESOLUTION 12 // 12-bit (0–4095)
#define QUEUE_SIZE 5
// Create an handle for the queue
QueueHandle_t potQueue = NULL;
void potTask(void *parameter) {
for (;;) {
uint16_t potValue = analogRead(POT_PIN); // Read 0–4095
xQueueSend(potQueue, &potValue, portMAX_DELAY); // Send to queue
Serial.printf("potTask: Sent pot value %u\n", potValue);
vTaskDelay(100 / portTICK_PERIOD_MS); // 100ms
}
}
void LEDBrightnessTask(void *parameter) {
for (;;) {
uint16_t potValue;
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
uint16_t brightness = potValue;
ledcWrite(LED_PIN, brightness);
Serial.printf("LEDBrightnessTask: Set brightness %u\n", brightness);
}
}
}
void setup() {
Serial.begin(115200);
// Setup PWM for LED
ledcAttach(LED_PIN, PWM_FREQ, PWM_RESOLUTION);
// Create queue (5 items, each uint16_t)
potQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
if (potQueue == NULL) {
Serial.println("Failed to create queue!");
while (1);
}
// Create tasks
xTaskCreatePinnedToCore(
potTask,
"potTask",
3000, // Task stack
NULL,
1,
NULL,
1 // Core 1
);
xTaskCreatePinnedToCore(
LEDBrightnessTask,
"LEDBrightnessTask",
3000,
NULL,
1,
NULL,
1
);
}
void loop() {
// Empty
}
How Does the Code Work?
Let’s take a look at the code to see how it works.
Define the LED and Potentiometer GPIOs
First define the GPIO pins for the LED and for the potentiometer.
#define POT_PIN 15
#define LED_PIN 2
PWM Properties
Then, define the PWM frequency and resolution to adjust the LED brightness. We’re setting 12-bit resolution (the same default resolution for the analog reading).
#define PWM_FREQ 5000
#define PWM_RESOLUTION 12 // 12-bit (0–4095)
If you’re new to PWM with the ESP32, check this tutorial to learn more: ESP32 PWM with Arduino IDE (Analog Output).
FreeRTOS Queue Size
Define the size of the FreeRTOS queue. This is the number of items that can the queue can hold. In this case, we’re setting it to 5.
#define QUEUE_SIZE 5
In our case, this is a good estimate because the items sent to the queue will be almost imediately consumed by the other task.
Queue Handle
Create an handle for the queue. This is how we’ll refer to our queue throughout our code. We’ll refer to the queue as potQueue.
// Create an handle for the queue
QueueHandle_t potQueue = NULL;
potTask Function
Create a function for the pontentiometer task: potTask. This is the task that will read from the pontentiometer.
void potTask(void *parameter) {
for (;;) {
uint16_t potValue = analogRead(POT_PIN); // Read 0–4095
xQueueSend(potQueue, &potValue, portMAX_DELAY); // Send to queue
Serial.printf("potTask: Sent pot value %u\n", potValue);
vTaskDelay(100 / portTICK_PERIOD_MS); // 100ms
}
}
We start by reading the value from the potentiometer and save it in a uint16_t type of variable called potValue.
uint16_t potValue = analogRead(POT_PIN); // Read 0–4095
To learn more about reading analog values with the ESP32, check out this tutorial: ESP32 ADC – Read Analog Values with Arduino IDE.
Send an Item to the Queue
Then, we’ll use the xQueueSend() function to send an item to the queue. We’re sending the potValue to the potQueue as follows.
Serial.printf("potTask: Sent pot value %u\n", potValue);
The last parameter, portMAX_DELAY means that the task will wait indefinitely until there is space in the queue for this new item. If you don’t want it to wait, and want to return immmediately, pass 0 instead.
vTaskDelay
Then, we wait 100 milliseconds between each reading.
vTaskDelay(100 / portTICK_PERIOD_MS); // 100ms
In FreeRTOS tasks, we must use the vTaskDelay() function instead of the usual delay() arduino function. It accepts as argument the number of ticks to wait. In case of the ESP32, one tick corresponds to 1ms=portTICK_PERIOD_MS.
LEDBrightnessTask
The following lines create the function for the task that controls the LED brightness. It’s called LEDBrightnessTask.
void LEDBrightnessTask(void *parameter) {
for (;;) {
uint16_t potValue;
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
uint16_t brightness = potValue;
ledcWrite(LED_PIN, brightness);
Serial.printf("LEDBrightnessTask: Set brightness %u\n", brightness);
}
}
}
We create a local variable called potValue that will receive the values in the queue.
uint16_t potValue;
Get Values From the Queue
To get values from the queue we can use the xQueueReceive() function. Pass the queue handle as an argument, a pointer to the variable where the value should be saved and whether to wait for an item to be available (portMAX_DELAY) or to return immediately (pass 0).
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
Control the LED Brightness
Because we’re controlling PWM with 12-bit resolution and using the default resolution for PWM (12-bit), the brightness will the be the same as the potValue.
uint16_t brightness = potValue;
Finally, we can use the ledcWrite() function to adjust the brightness of the LED.
ledcWrite(LED_PIN, brightness);
setup()
In the setup(), initialize the Serial Monitor at a baud rate of 115200.
Serial.begin(115200);
Set up the PWM properties.
ledcAttach(LED_PIN, PWM_FREQ, PWM_RESOLUTION);
Creating the Queue
Create the actual queue using the xQueueCreate() function. This accepts as argument the queue size and the size of each item in bytes. Since we’re storing uint16_t variables, we’re passing the size of uint16_t, which is 2 bytes (16 bits).
// Create queue (5 items, each uint16_t)
potQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
if (potQueue == NULL) {
Serial.println("Failed to create queue!");
while (1);
}
Creating the Tasks
Then, create the tasks that handle the potentiometer and control the LED brightness. We’re assigning both tasks to core 1.
// Create tasks
xTaskCreatePinnedToCore(
potTask,
"potTask",
3000, // Task stack
NULL,
1,
NULL,
1 // Core 1
);
xTaskCreatePinnedToCore(
LEDBrightnessTask,
"LEDBrightnessTask",
3000,
NULL,
1,
NULL,
1
);
We covered how to create tasks in this previous tutorial: ESP32 with FreeRTOS (Arduino IDE) – Getting Started: Create Tasks
loop()
The loop() is empty because FreeRTOS handles the tasks.
void loop() {
// Empty
}
Demonstration
Upload the code to your board. Press the onboard RST button, so that the ESP32 starts running the code.
Then, open the Serial Monitor at a baud rate of 115200. You should get something similar.

Rotate the potentiometer and see the LED brightness increase and decrease accordingly.

Example 2: FreeRTOS Queues – Sharing Data Between Three Tasks
Data can be shared between multiple FreeRTOS tasks. In the previous example, we passed data from one task to another. But we can pass data to as many tasks as we want.
This example is a modification of the previous project, with the addition of a third task. This third task will be responsible for retrieving the potentiometer values from the queue and displaying the values along with a timestamp in the Serial Monitor. I also removed all Serial prints from the other tasks.
This is a simple example that shows how you can use queues to share data between multiple tasks.
Wiring the Circuit
Keep the same circuit with an LED and a potentiometer from the previous example.
Code: FreeRTOS Queues – Three Tasks
Copy the following code to the Arduino IDE.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-queues-inter-task-arduino/
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 POT_PIN 15
#define LED_PIN 2
#define PWM_FREQ 5000
#define PWM_RESOLUTION 12 // 12-bit (0–4095)
#define QUEUE_SIZE 5
QueueHandle_t potQueue = NULL;
void SensorTask(void *parameter) {
for (;;) {
uint16_t potValue = analogRead(POT_PIN); // Read 0–4095
xQueueSend(potQueue, &potValue, portMAX_DELAY); // Send to queue
vTaskDelay(300 / portTICK_PERIOD_MS); // 100ms
}
}
void LEDBrightnessTask(void *parameter) {
for (;;) {
uint16_t potValue;
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
uint16_t brightness = potValue;
ledcWrite(LED_PIN, brightness);
}
}
}
void SerialLoggerTask(void *parameter) {
for (;;) {
uint16_t potValue;
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
Serial.printf("SerialLoggerTask: Pot value %u at %lu ms\n", potValue, millis());
}
}
}
void setup() {
Serial.begin(115200);
// Setup PWM for LED
ledcAttach(LED_PIN, PWM_FREQ, PWM_RESOLUTION);
// Create queue (5 items, each uint16_t)
potQueue = xQueueCreate(QUEUE_SIZE, sizeof(uint16_t));
if (potQueue == NULL) {
Serial.println("Failed to create queue!");
while (1);
}
// Create tasks with one parameter per line
xTaskCreatePinnedToCore(
SensorTask, // Task function
"SensorTask", // Task name
3000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
LEDBrightnessTask, // Task function
"LEDBrightnessTask", // Task name
3000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
SerialLoggerTask, // Task function
"SerialLoggerTask", // Task name
3000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
// Empty
}
How Does the Code Work?
This code is very similar to the previous example, but we add an additional task. The SerialLoggerTask().
void SerialLoggerTask(void *parameter) {
for (;;) {
uint16_t potValue;
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
Serial.printf("SerialLoggerTask: Pot value %u at %lu ms\n", potValue, millis());
}
}
}
This task gets the potValue from the potQueue.
if (xQueueReceive(potQueue, &potValue, portMAX_DELAY)) {
And then, displays it in the Serial Monitor along a timestamp.
Serial.printf("SerialLoggerTask: Pot value %u at %lu ms\n", potValue, millis());
You also need to create this new task in the setup().
xTaskCreatePinnedToCore(
SerialLoggerTask, // Task function
"SerialLoggerTask", // Task name
3000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
Demonstration
The result will be similar to the previous example, but now you have the SerialLoggerTask displaying the values and the timestamp in the Serial Monitor.

This simple example is to demonstrate that you can pass data to as many tasks as you want with multiple queues.
Example 3: Display Sensor Data on a Screen (Sharing a Data Structure)

In this example, we’ll display sensor readings from a BME280 on an OLED display using FreeRTOS tasks. The process is divided into two main tasks:
SensorTask:
- Reads temperature, humidity, and pressure from the BME280 sensor
- Sends the sensor data to a queue
DisplayTask
- Receives sensor data from the queue
- Displays the readings on the OLED screen
The aim of this example is to demonstrate how to share a data structure containing multiple variables (temperature, humidity, and pressure) between tasks.
Parts Required
For this example, you need the following parts:
- ESP32 Board of your choice – read Best ESP32 Development Boards
- BME280 Sensor
- SSD1306 OLED display
- Breadboard
- Jumper wires
Wiring the Circuit
Wire the BME280 and the OLED display to the ESP32 default I2C pins.

You may also like reading: ESP32 I2C Communication: Set Pins, Multiple Bus Interfaces and Peripherals (Arduino IDE).
BME280 | ESP32 |
VIN | 3V3 |
GND | GND |
SCL | GPIO 22 |
SDA | GPIO 21 |
OLED Display | ESP32 |
VIN | 3V3 |
GND | GND |
SCL | GPIO 22 |
SDA | GPIO 21 |
Code – Display Sensor Data on a Screen using FreeRTOS Queues
Copy the following code to the Arduino IDE.
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Complete project details at https://RandomNerdTutorials.com/esp32-freertos-queues-inter-task-arduino/
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.
*/
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDRESS 0x3C
#define QUEUE_SIZE 5
Adafruit_BME280 bme;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
QueueHandle_t sensorQueue = NULL;
typedef struct {
float temperature;
float humidity;
float pressure;
} SensorData;
void SensorTask(void *parameter) {
if (!bme.begin(0x76)) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
for (;;) {
SensorData data;
data.temperature = bme.readTemperature();
data.humidity = bme.readHumidity();
data.pressure = bme.readPressure() / 100.0F;
xQueueSend(sensorQueue, &data, portMAX_DELAY);
Serial.print("SensorTask: Sent Temp=");
Serial.print(data.temperature, 1);
Serial.print("°C, Hum=");
Serial.print(data.humidity, 1);
Serial.print("%, Pres=");
Serial.print(data.pressure, 1);
Serial.println("hPa");
vTaskDelay(2000 / portTICK_PERIOD_MS); // 2s
}
}
void DisplayTask(void *parameter) {
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("OLED init failed!");
while (1);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
for (;;) {
SensorData data;
if (xQueueReceive(sensorQueue, &data, portMAX_DELAY)) {
// Display on OLED
display.clearDisplay();
display.setCursor(0, 0);
display.print("Temperature: ");
display.print(data.temperature, 1);
display.print(" ");
display.cp437(true);
display.write(167);
display.println("C");
display.setCursor(0, 20);
display.print("Humidity: ");
display.print(data.humidity, 1);
display.println(" %");
display.setCursor(0, 40);
display.print("Pressure: ");
display.print(data.pressure, 1);
display.println(" hPa");
display.display();
}
}
}
void setup() {
Serial.begin(115200);
// Starts I2C on the board default's I2C pins
Wire.begin();
// Create queue
sensorQueue = xQueueCreate(QUEUE_SIZE, sizeof(SensorData));
if (sensorQueue == NULL) {
Serial.println("Failed to create queue!");
while (1);
}
// Create tasks
xTaskCreatePinnedToCore(
SensorTask, // Task function
"SensorTask", // Task name
4000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
DisplayTask, // Task function
"DisplayTask", // Task name
4000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
}
void loop() {
// Empty
}
How Does the Code Work?
At this point, you should be familiar with most of the code.
To send multiple variables to the queue, we create a structure that will contain temperature, humidity, and pressure. We call that type of structure SensorData.
Alternatively, you can also use JSON objects, but I find structures easier to use for this scenario.
typedef struct {
float temperature;
float humidity;
float pressure;
} SensorData;
The SensorTask function is responsible for retrieving the current sensor readings and sending them to the queue. We create a variable called data, which is a data structure of type SensorData (the one we created previously).
SensorData data;
Then, we add values to the data structure as follows.
data.temperature = bme.readTemperature();
data.humidity = bme.readHumidity();
data.pressure = bme.readPressure() / 100.0F;
Now that we have populated the data structure with the latest sensor readings, we can send it to the queue, just as we did in the previous examples, using the xQueueSend() function.
xQueueSend(sensorQueue, &data, portMAX_DELAY);
In the DisplayTask function, we get the data from the queue and display it on the OLED screen.
void DisplayTask(void *parameter) {
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
Serial.println("OLED init failed!");
while (1);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
for (;;) {
SensorData data;
if (xQueueReceive(sensorQueue, &data, portMAX_DELAY)) {
// Display on OLED
display.clearDisplay();
display.setCursor(0, 0);
display.print("Temperature: ");
display.print(data.temperature, 1);
display.print(" ");
display.cp437(true);
display.write(167);
display.println("C");
display.setCursor(0, 20);
display.print("Humidity: ");
display.print(data.humidity, 1);
display.println(" %");
display.setCursor(0, 40);
display.print("Pressure: ");
display.print(data.pressure, 1);
display.println(" hPa");
display.display();
}
}
}
As in the previous examples, in the setup(), we create the queue and the tasks, and assign them to an ESP32 core.
void setup() {
Serial.begin(115200);
// Starts I2C on the board default's I2C pins
Wire.begin();
// Create queue
sensorQueue = xQueueCreate(QUEUE_SIZE, sizeof(SensorData));
if (sensorQueue == NULL) {
Serial.println("Failed to create queue!");
while (1);
}
// Create tasks
xTaskCreatePinnedToCore(
SensorTask, // Task function
"SensorTask", // Task name
4000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
xTaskCreatePinnedToCore(
DisplayTask, // Task function
"DisplayTask", // Task name
4000, // Stack size (bytes)
NULL, // Task parameters
1, // Priority
NULL, // Task handle
1 // Core ID
);
}
The loop() is empty because FreeRTOS handles the tasks.
void loop() {
// Empty
}
Demonstration
Upload the previous code to your ESP32 board. After uploading, press the onboard RST button so that the board starts running the code.
Open the Serial Monitor at a baud rate of 115200. Every two seconds, it will display updated sensor readings.

The screen is updated at the same rate as new readings are sent to the queue.

Wrapping Up
In this tutorial, you learned how to use FreeRTOS queues to pass data from one task to the other in a safe way. We’ve shown you three different examples that demonstrate how easy it is to use queues to communicate between tasks. After understanding those basic examples, the idea is to apply this concept to more complex projects, where accessing variables’ values simultaneously might be critical to the program.
We hope you’ve found this tutorial useful. We’ll be creating more FreeRTOS guides with the ESP32 programmed with Arduino IDE soon. So, stay tuned.
Learn more about the ESP32 with our resources:
- ESP32 with FreeRTOS (Arduino IDE) – Getting Started: Create Tasks
- Learn ESP32 with Arduino IDE (eBook)
- All our ESP32 Projects and Guides
Thanks for reading.