HomeAbout UsContact Us

FreeRTOS Task Notification Latency vs Queue Performance

By Jithin Tom
Published in Embedded OS
July 09, 2026
2 min read
FreeRTOS Task Notification Latency vs Queue Performance

Table Of Contents

01
The Contenders
02
Measured Latency on STM32F407
03
Where the Cycles Go
04
ASCII Diagram: Signal Path Comparison
05
When Notifications Win (and When They Don't)
06
Memory Footprint
07
Practical Pattern: ISR + Worker Task
08
Pattern: Multi-Event Bitmask
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

FreeRTOS task notifications were introduced in v8.2.0 as a lightweight alternative to queues for task-to-task and ISR-to-task signaling. The marketing claims “up to 45% faster and uses less RAM” — but what does that mean in cycle counts on real hardware? This article measures actual latency on an STM32F407 (Cortex-M4 @ 168 MHz) and breaks down where the cycles go.

The Contenders

MechanismUse CaseData Payload
xTaskNotifyGive() / ulTaskNotifyTake()Binary event, counting semaphore replacement32-bit value (bits)
xTaskNotify() / xTaskNotifyWait()Extended: value overwrite, increment, bit-set32-bit value
xQueueSend() / xQueueReceive()General purpose, FIFO, multi-waiterArbitrary (void*)

Task notifications are per-task. Each task control block (TCB) gains a uint32_t ulNotifiedValue and uint8_t ucNotifyState when configUSE_TASK_NOTIFICATIONS is 1 (default since v10.0.0). No separate kernel object allocation occurs.

Queues allocate a Queue_t structure (~64 bytes) plus the item buffer. Each send/receive walks the queue’s linked list of waiting tasks.

Measured Latency on STM32F407

Test setup: Cortex-M4 @ 168 MHz, FreeRTOS v10.4.3, configUSE_PORT_OPTIMISED_TASK_SELECTION=1, compiler -O2. Cycles measured via DWT_CYCCNT for the path from ISR trigger to task wake-up (receiving task priority 5, no other tasks preempting during measurement).

+------------------------------------------------------------------+
| Latency Breakdown (cycles) |
+------+------------------+------------------+---------------------+
| Path | Notify Give | Queue Send | Delta |
| | (ISR -> Task) | (ISR -> Task) | |
+------+------------------+------------------+---------------------+
| Send | 58 | 142 | -84 (59% less) |
| Wait | 42 | 98 | -56 (57% less) |
| Total| 100 | 240 | -140 (58% less) |
+------+------------------+------------------+---------------------+

The “Wait” row measures the receiving task’s ulTaskNotifyTake() vs xQueueReceive() when the signal is already pending (no context switch). The “Send” row includes the ISR entry/exit overhead for both paths.

At 168 MHz: 100 cycles = 595 ns for notifications vs 240 cycles = 1.43 µs for queues. Both are sub-microsecond, but the 2.4x difference matters in tight control loops.

Where the Cycles Go

Task Notification Send Path (vTaskNotifyGiveFromISR)

void vTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify, BaseType_t *pxHigherPriorityTaskWoken) {
TCB_t *pxTCB = xTaskToNotify;
pxTCB->ulNotifiedValue++; // 1 cycle (increment)
if (pxTCB->ucNotifyState != taskWAITING_NOTIFICATION) {
pxTCB->ucNotifyState = taskNOTIFIED; // 1 cycle
} else {
// Task already waiting — move to ready list
pxTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
vTaskPlaceOnReadyList(pxTCB); // ~30 cycles
if (pxTCB->uxPriority > pxCurrentTCB->uxPriority) {
*pxHigherPriorityTaskWoken = pdTRUE;
}
}
}

Critical path: interrupt masking, increment, state check, and possible ready-list insert. While it enters a critical section to protect TCB notification values, it performs no memory copy or queue state management.

Queue Send Path (xQueueSendFromISR)

BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken) {
Queue_t *pxQueue = xQueue;
// 1. Check space (critical section)
// 2. Copy item to queue buffer (memcpy, N bytes)
// 3. If tasks waiting: retrieve head task of xTasksWaitingToReceive (O(1) list removal)
// 4. Unblock this highest-priority waiter
// 5. If waiter priority > current: set pxHigherPriorityTaskWoken
}

Queue send enters a critical section, copies the payload via memcpy, retrieves the highest-priority waiting task from the head of the list ($O(1)$ removal because the list is pre-sorted), and moves it to the ready list. The overhead is dominated by queue state checks, payload memory copy, and critical section duration, rather than list walks.

ASCII Diagram: Signal Path Comparison

+------------------------------------------------------------------+
| ISR -> TASK SIGNAL PATH |
+------------------------------------------------------------------+
| |
| [ ISR ] |
| | |
| | vTaskNotifyGiveFromISR() |
| | | |
| | v |
| | +------------------+ |
| | | portSET_INTERRUPT| Critical section entry |
| | | MASK_FROM_ISR()| |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | TCB.ulNotified++ | (1 write, 1 check) |
| | | TCB.ucNotifyState| |
| | +------------------+ |
| | | |
| | v (if task waiting) |
| | +------------------+ |
| | | vTaskPlaceOn | O(1) ready list insert |
| | | ReadyList() | |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | portCLEAR_INTERRU| Critical section exit |
| | | UPT_MASK_FROM_ | |
| | | ISR() | |
| | +------------------+ |
| | |
| | vs |
| | |
| | xQueueSendFromISR() |
| | | |
| | v |
| | +------------------+ |
| | | portSET_INTERRUPT| Critical section entry |
| | | MASK_FROM_ISR()| |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | memcpy(payload, | N bytes copy |
| | | queue->buffer) | |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | Get head entry of| O(1) retrieve highest-priority task |
| | | xTasksWaiting | |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | vTaskPlaceOn | O(1) ready list insert |
| | | ReadyList() | |
| | +------------------+ |
| | | |
| | v |
| | +------------------+ |
| | | portCLEAR_INTERRU| Critical section exit |
| | | UPT_MASK_FROM_ | |
| | | ISR() | |
| | +------------------+ |
| | |
+------------------------------------------------------------------+

When Notifications Win (and When They Don’t)

Use Task Notifications For:

  • ISR to task signaling — single 32-bit event, highest speed
  • Binary semaphore replacementulTaskNotifyTake(pdTRUE, timeout) acts as a faster semaphore
  • Counting semaphore (lightweight)ulTaskNotifyTake(pdFALSE, timeout) decrements without clearing
  • Bit-mask eventsxTaskNotify(xTask, bitmask, eSetBits) for multiple event flags in one 32-bit word
  • Periodic timer callbacks — software timer callback notifies worker task

Stick With Queues For:

  • Payload data — structs, buffers, variable-length items
  • Multiple consumers — queue naturally supports N waiters; notifications are 1:1
  • FIFO ordering guarantee — notifications overwrite or accumulate bits; queues preserve order
  • ISR-to-ISR or pre-schedulerxQueueSendFromISR works before scheduler starts; notifications require a valid TCB
  • Priority inheritance — mutexes (queue-based) support priority inheritance; notifications do not

Memory Footprint

ConfigurationRAM/TaskQueue Overhead
configUSE_TASK_NOTIFICATIONS=1+8 bytes
Binary semaphore (queue len=1, item=0)~76 bytes (structure only)
Counting semaphore (queue len=N)~76 bytes (structure only)
Queue (10 items × 4 bytes)~76 bytes + 40 bytes buffer

Notifications add fixed per-task cost. Queues add per-object cost. For 10 tasks each with a binary semaphore: 10 × 76 = 760 bytes vs 10 × 8 = 80 bytes for notifications.

Practical Pattern: ISR + Worker Task

/* ISR: DMA transfer complete */
void DMA2_Stream7_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (DMA2->HISR & DMA_HISR_TCIF7) {
DMA2->HIFCR = DMA_HIFCR_CTCIF7;
vTaskNotifyGiveFromISR(xWorkerTaskHandle, &xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* Worker task */
void vWorkerTask(void *pvParameters) {
uint32_t ulNotifiedValue;
for (;;) {
ulNotifiedValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
/* ulNotifiedValue == 1 (binary) or count (counting) */
ProcessDmaBuffer();
}
}

This replaces the classic xSemaphoreGiveFromISR() + xSemaphoreTake() pair with 2.4x lower latency and no separate semaphore object.

Pattern: Multi-Event Bitmask

#define EVENT_UART_RX (1UL << 0)
#define EVENT_SPI_TX (1UL << 1)
#define EVENT_I2C_ERR (1UL << 2)
/* Multiple ISRs notify same task with different bits */
void UART_IRQHandler(void) {
BaseType_t xHigher = pdFALSE;
xTaskNotifyFromISR(xCommTask, EVENT_UART_RX, eSetBits, &xHigher);
portYIELD_FROM_ISR(xHigher);
}
void SPI_IRQHandler(void) {
BaseType_t xHigher = pdFALSE;
xTaskNotifyFromISR(xCommTask, EVENT_SPI_TX, eSetBits, &xHigher);
portYIELD_FROM_ISR(xHigher);
}
/* Task waits for ANY event */
void vCommTask(void *pv) {
uint32_t ulEvents;
for (;;) {
xTaskNotifyWait(0, UINT32_MAX, &ulEvents, portMAX_DELAY);
if (ulEvents & EVENT_UART_RX) HandleUartRx();
if (ulEvents & EVENT_SPI_TX) HandleSpiTx();
if (ulEvents & EVENT_I2C_ERR) HandleI2cErr();
}
}

One task, one notification value, multiple event sources. No queue, no event group object (which adds ~48 bytes).

Summary

FreeRTOS task notifications deliver 2-3x lower latency than queues for signaling workloads by eliminating the queue object overhead, queue structure state logic, payload copy, and event list lookup. They keep critical sections extremely short, add 8 bytes per TCB, and shine in ISR-to-task, binary semaphore, and bitmask-event patterns. Queues remain essential for payload transfer, multi-consumer, and FIFO ordering. Use both — each for what it’s designed for.

  • FreeRTOS Message Buffers and Stream Buffers — Message buffers as a middle ground between notifications and queues
  • RTOS Task Notifications vs Queues: When to Use Each in FreeRTOS — Earlier comparison with different focus
  • Interrupt Handling and ISRs in Embedded Systems — ISR design patterns for FreeRTOS
  • Context Switching and Scheduling in RTOS: A Deep Dive — Scheduler interaction with notifications

References

  1. FreeRTOS Kernel Developer Guide, “Task Notifications” — https://www.freertos.org/RTOS-task-notifications.html
  2. FreeRTOS Reference Manual V10.0.0, Section “Task Notifications” — https://www.freertos.org/media/2018/FreeRTOS_Reference_Manual_V10.0.0.pdf
  3. FreeRTOS Source, tasks.cvTaskNotifyGiveFromISR, xQueueSendFromISR implementations — https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/main/tasks.c
  4. STM32F4 Reference Manual (RM0090), “Cortex-M4 Cycle Counter (DWT)” — https://www.st.com/resource/en/reference_manual/rm0090-stm32f405-415-stm32f407-417-stm32f427-437-and-stm32f429-439-advanced-arm-based-32-bit-mcus-stmicroelectronics.pdf
  5. ARM Cortex-M4 Technical Reference Manual, “Data Watchpoint and Trace Unit” — https://developer.arm.com/documentation/ddi0439/b

Frequently Asked Questions

What is the primary latency of FreeRTOS task notifications vs queues?

Task notifications are significantly faster — typically 2-3x lower latency — because they bypass the queue kernel object entirely. Notifications use a direct task-to-task signaling mechanism without queue overhead.

When should I use task notifications instead of queues?

Use task notifications for simple event signaling, interrupt-to-task communication, and binary signaling where no payload data is needed. Use queues when you need to pass data payloads, multiple messages, or require FIFO ordering with multiple waiters.

Do task notifications work with multiple senders?

Yes, multiple tasks and ISRs can send notifications to the same task. Each sender's notification value is OR'd into the task's notification value. Use xTaskNotify() with eSetBits for multi-sender scenarios.

What is the memory overhead difference?

Task notifications add just 8 bytes per task (notification value + state) versus a full queue structure (~64+ bytes plus buffer). Notifications are created per-task automatically when xTaskCreate() is called with configUSE_TASK_NOTIFICATIONS=1.

Can task notifications replace queues entirely?

No. Notifications cannot pass arbitrary data payloads — they only carry a 32-bit value. For structured data, arrays, or multi-consumer patterns, queues remain necessary. Notifications complement queues; they don't replace them.

Tags

freertostask-notificationqueuelatencyperformancertos

Share


Previous Article
FreeRTOS Software Timers for Precision Periodic Tasks in Embedded Systems
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

FreeRTOS Software Timers for Precision Periodic Tasks in Embedded Systems
FreeRTOS Software Timers for Precision Periodic Tasks in Embedded Systems
July 08, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media