
When designing communication between tasks in a FreeRTOS-based embedded system, queues and semaphores are often the first mechanisms that come to mind. However, FreeRTOS provides a lesser-known yet powerful alternative: direct-to-task notifications. Understanding when to use task notifications instead of queues can lead to measurable improvements in both performance and memory utilization — critical factors in resource-constrained embedded systems.
This article explores the differences between task notifications and queues, outlines the strengths and limitations of each, and provides practical guidance on choosing the right mechanism for common embedded design patterns.
Every FreeRTOS task has a built-in 32-bit notification value and a notification state that is either pending or not-pending. Task notifications allow one task (or ISR) to send an event directly to a specific task without requiring a separate communication object like a queue or semaphore.
The feature is enabled by setting configUSE_TASK_NOTIFICATIONS to 1 in FreeRTOSConfig.h. When enabled, the overhead is minimal — just 5 bytes per task by default (configTASK_NOTIFICATION_ARRAY_ENTRIES * 5 bytes).
The simplest API pair for ping-pong style synchronization is xTaskNotifyGive() (sender) and ulTaskNotifyTake() (receiver):
/* Sensor task signals the processing task */void sensor_task(void *param) {while (1) {read_sensor_data();xTaskNotifyGive(xProcessingTaskHandle);vTaskDelay(pdMS_TO_TICKS(100));}}/* Processing task waits for notification */void processing_task(void *param) {while (1) {ulTaskNotifyTake(pdTRUE, portMAX_DELAY);process_sensor_data();}}
This pattern replaces a binary or counting semaphore with zero additional RAM overhead beyond the task’s own TCB.
| Aspect | Task Notifications | Queues |
|---|---|---|
| RAM overhead | 5 bytes per task (fixed) | ~80+ bytes per queue (struct + buffer) |
| Speed | ~45% faster than semaphore | Slower due to copy and list operations |
| Direction | Sender knows the receiver | Any task can read/write |
| Data capacity | One 32-bit value | Multiple items, any size |
| Buffering | Single notification (overwrites) | FIFO buffer of N items |
| Multiple receivers | Not supported | Supported |
| ISR safe | xTaskNotifyGiveFromISR() | xQueueSendFromISR() |
| Broadcast | Not supported | Acceptable with multiple readers |
+=======================================================+| TASK NOTIFICATION || || +----------------+ 5 bytes RAM || | 32-bit value | ~45% faster || | (one-slot) | Point-to-point only || +----------------+ || || Sender ------------notification------------> || (direct to TCB) Receiver |+=======================================================++=======================================================+| QUEUE || || +-----+-----+-----+-----+-----+ || | 1 | 2 | 3 | 4 | 5 | <- FIFO buffer || +-----+-----+-----+-----+-----+ (~80+ bytes) || || Writer(s) ------send------> [===buffer===] ----> || Any reader can pop || Reader 1 <------pop------- || Reader 2 <------pop------- |+=======================================================+
The notification writes directly into the task’s TCB — no kernel object, no list traversal, no data copy. A queue must allocate a buffer, manage head/tail pointers, and copy data in and out.
According to the FreeRTOS kernel documentation, unblocking a task with a direct notification is 45% faster than using a binary semaphore, and uses significantly less RAM. The reason is straightforward: there is no separate kernel object to create, no list to manage, and no data copying. The notification is written directly into the task’s TCB (Task Control Block).
The most important limitation of task notifications is that they are inherently point-to-point. Only one task can be the recipient. If you need to broadcast an event to multiple tasks, or if the receiver is not known at compile time, a queue or event group is the correct choice.
Additionally, the 32-bit notification value can hold only one value at a time. If the sender issues multiple notifications before the receiver processes them, the behavior depends on the notification action — increment actions count pending notifications, but overwrite actions replace the value.
The xTaskNotify() function supports four actions that give task notifications their versatility:
/* 1. Pure signal (like binary semaphore) */xTaskNotify(xTaskHandle, 0, eNoAction);/* 2. Increment (like counting semaphore) */xTaskNotify(xTaskHandle, 0, eIncrement);/* 3. Set bits (like event group) */xTaskNotify(xTaskHandle, 0x01, eSetBits);/* 4. Overwrite value (like single-slot mailbox) */xTaskNotify(xTaskHandle, sensor_value, eSetValueWithOverwrite);
When different bits in the notification value represent different event flags, task notifications serve as a lightweight event group:
/* ISR sets event flags */void UART3_IRQHandler(void) {BaseType_t xHigherPriorityTaskWoken = pdFALSE;xTaskNotifyFromISR(xCommTaskHandle, 0x01, eSetBits,&xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}/* Task waits for any combination of flags */void comm_task(void *param) {uint32_t notified_value;while (1) {xTaskNotifyWait(0x00, /* clear nothing on entry */0xFFFFFFFF, /* clear all on exit */¬ified_value,portMAX_DELAY);if (notified_value & 0x01) {handle_uart_event();}if (notified_value & 0x02) {handle_timer_event();}}}
This eliminates the need for a separate event group object (which costs ~60 bytes of RAM) when only one task needs to process the events.
Task notifications are not a universal replacement for queues. Use queues when:
For most embedded systems, a hybrid approach works best. Use task notifications for simple synchronization and signaling (ISR-to-task, task-to-task handshakes), and reserve queues for data streaming and multi-consumer scenarios.
A good rule of thumb: if you find yourself creating a binary semaphore solely to unblock one specific task, replace it with a task notification. The RAM savings accumulate quickly in systems with many synchronization points.
Always profile on your target. The performance difference is most pronounced on fast Cortex-M cores where the overhead of queue operations becomes a measurable fraction of total CPU time.
Task notifications in FreeRTOS offer a compelling combination of low RAM usage and high speed for point-to-point task communication. They can replace binary semaphores, counting semaphores, event groups, and even single-slot queues in many common scenarios. The trade-off is limited flexibility — one receiver, one 32-bit value, no built-in broadcast. For embedded systems where every byte of RAM and every cycle of CPU time matters, mastering task notifications is an essential skill that leads to cleaner, leaner RTOS designs.
Quick Links
Legal Stuff




