
FreeRTOS uses a fixed-priority preemptive scheduler. By design, the highest-priority ready task always runs. When higher-priority tasks consume all available CPU time, lower-priority tasks never execute — this is task starvation. It’s not a bug; it’s the scheduler working as specified. But in production systems, starvation breaks watchdog timers, misses deadlines on background housekeeping, and causes silent data corruption in logging or communication buffers.
This article covers why starvation happens, why priority inheritance alone isn’t enough, and how to implement priority aging and boosting in FreeRTOS with working code.
+------------------------------------------------------------------+| PRIORITY-BASED PREEMPTION |+------------------------------------------------------------------+| Priority 5 (High) | ######################################## | Runs 95% of time| Priority 4 | #################### | Runs 4% of time| Priority 3 | ############ | Runs 1% of time| Priority 2 | ## | Starving| Priority 1 (Low) | # | Starving| Priority 0 (Idle) | ........................................ | Never runs+------------------------------------------------------------------+
In a fixed-priority system without aging or boosting:
vApplicationIdleHook() never fires -> no background cleanupThe scheduler has no built-in mechanism to say “this task has waited long enough, let it run.”
FreeRTOS priority inheritance (enabled via configUSE_MUTEXES=1 and using SemaphoreHandle_t mutexes) solves priority inversion:
Task L (low) holds mutex --> Task H (high) blocks on mutex|vTask L inherits Task H's priority|vTask L runs, releases mutex|vTask H acquires, runs
This is essential for mutex-protected critical sections. But it only triggers when a high-priority task blocks on a mutex held by a lower-priority task. It does nothing when:
Starvation is a system-wide CPU allocation problem, not a mutex-specific inversion problem. You need both mechanisms.
Aging increments the effective priority of tasks that haven’t run recently. Since FreeRTOS does not provide an ISR-safe API to change task priorities (vTaskPrioritySetFromISR does not exist), you cannot do this in the tick hook (vApplicationTickHook). Instead, use a dedicated, high-priority periodic task to monitor and age other tasks.
/* FreeRTOSConfig.h prerequisites */#define INCLUDE_eTaskGetState 1#define INCLUDE_vTaskPrioritySet 1 /* usually 1 by default *//* Configuration */#define AGING_INTERVAL_MS 200 /* Boost every 200ms */#define AGING_MAX_PRIORITY_BOOST 3 /* Max +3 priority levels */#define AGING_MIN_PRIORITY 1 /* Don't boost idle */#define AGING_MAX_TASKS 16/* Task handle array for aging -- populate at task creation */static TaskHandle_t aging_task_handles[AGING_MAX_TASKS];static uint8_t aging_task_count = 0;static uint32_t aging_wait_cycles[AGING_MAX_TASKS];static UBaseType_t aging_original_priorities[AGING_MAX_TASKS];/* Register a task for aging -- call once after xTaskCreate() */void aging_register_task(TaskHandle_t task) {if (aging_task_count < AGING_MAX_TASKS) {aging_task_handles[aging_task_count] = task;aging_wait_cycles[aging_task_count] = 0;aging_original_priorities[aging_task_count] = uxTaskPriorityGet(task);aging_task_count++;}}/* Aging Task -- Runs periodically at a high priority */void aging_monitor_task(void *pvParameters) {TickType_t xLastWakeTime = xTaskGetTickCount();const TickType_t xFrequency = pdMS_TO_TICKS(AGING_INTERVAL_MS);for (;;) {vTaskDelayUntil(&xLastWakeTime, xFrequency);for (uint8_t i = 0; i < aging_task_count; i++) {TaskHandle_t h = aging_task_handles[i];if (h == NULL) continue;eTaskState state = eTaskGetState(h);/* Age tasks that are READY (waiting for CPU) */if (state == eReady) {aging_wait_cycles[i]++;UBaseType_t curr_prio = uxTaskPriorityGet(h);UBaseType_t orig_prio = aging_original_priorities[i];UBaseType_t max_boosted = orig_prio + AGING_MAX_PRIORITY_BOOST;UBaseType_t max_allowed = configMAX_PRIORITIES - 1;if (curr_prio < max_boosted && curr_prio < max_allowed) {/* Boost by one level per aging cycle */vTaskPrioritySet(h, curr_prio + 1);}} else {/* Task ran, blocked, or suspended -- reset counter, restore priority */aging_wait_cycles[i] = 0;UBaseType_t curr_prio = uxTaskPriorityGet(h);UBaseType_t orig_prio = aging_original_priorities[i];if (curr_prio > orig_prio) {vTaskPrioritySet(h, orig_prio);}}}}}
aging_register_task()AGING_INTERVAL_MS (e.g., 200ms). It must run at a very high priority so it doesn’t get starved itself.eReady state, it increments its priority.eReady during the next check, so its priority is restored.AGING_MAX_PRIORITY_BOOST levels above original — never exceed configMAX_PRIORITIES - 1.| Parameter | Typical Value | Rationale |
|---|---|---|
AGING_INTERVAL_MS | 100-500ms | Lower = faster response for starved tasks, higher = less priority thrashing |
AGING_MAX_PRIORITY_BOOST | 2-4 | Enough to break through medium-priority noise, not enough to starve others |
configMAX_PRIORITIES | >= 7 | Need headroom above your highest app priority for boosts to work |
TaskHandle_t log_task_handle;TaskHandle_t comms_task_handle;TaskHandle_t monitor_task_handle;void create_tasks(void) {xTaskCreate(log_task, "Log", 512, NULL, 1, &log_task_handle);xTaskCreate(comms_task, "Comms", 1024, NULL, 2, &comms_task_handle);xTaskCreate(monitor_task, "Mon", 512, NULL, 1, &monitor_task_handle);aging_register_task(log_task_handle);aging_register_task(comms_task_handle);aging_register_task(monitor_task_handle);/* Don't register idle or highest-priority control tasks */}
Aging is time-based and blind. Sometimes you know exactly when a low-priority task must run — after an interrupt, when a buffer fills, or when a watchdog window opens. Use event-driven boosting for these cases. Since you can’t change priorities from an ISR, you must defer the action to a daemon task, like the FreeRTOS Timer Service.
/* Requires configUSE_TIMERS=1, INCLUDE_xTimerPendFunctionCall=1,and INCLUDE_vTaskPrioritySet=1 in FreeRTOSConfig.h */#include "timers.h"/* Boost a task's priority temporarily, with auto-restore */typedef struct {TaskHandle_t task;UBaseType_t original_priority;uint32_t boost_ticks;bool active;} priority_boost_t;static priority_boost_t active_boosts[8];static uint8_t boost_count = 0;/* Internal function executed by the Timer Daemon Task */static void priority_boost_execute(void *pvParameter1, uint32_t ulParameter2) {TaskHandle_t task = (TaskHandle_t)pvParameter1;uint32_t duration_ticks = ulParameter2;if (boost_count >= 8) return;UBaseType_t orig = uxTaskPriorityGet(task);UBaseType_t boosted = orig + 2; /* Boost by 2 levels */if (boosted >= configMAX_PRIORITIES) boosted = configMAX_PRIORITIES - 1;vTaskPrioritySet(task, boosted);active_boosts[boost_count].task = task;active_boosts[boost_count].original_priority = orig;active_boosts[boost_count].boost_ticks = duration_ticks;active_boosts[boost_count].active = true;boost_count++;}/* Call from ISR to boost a task for N ticks safely */void priority_boost_task_from_isr(TaskHandle_t task, uint32_t duration_ticks, BaseType_t *pxHigherPriorityTaskWoken) {xTimerPendFunctionCallFromISR(priority_boost_execute,(void *)task,duration_ticks,pxHigherPriorityTaskWoken);}/* Call from a periodic task (e.g., aging task) to decrement and restore */void priority_boost_tick(void) {for (int i = 0; i < boost_count; i++) {if (!active_boosts[i].active) continue;if (active_boosts[i].boost_ticks > 0) {active_boosts[i].boost_ticks--;}if (active_boosts[i].boost_ticks == 0) {vTaskPrioritySet(active_boosts[i].task, active_boosts[i].original_priority);active_boosts[i].active = false;}}}/* Example: Boost log task when buffer > 80% full */void uart_rx_isr(void) {BaseType_t xHigherPriorityTaskWoken = pdFALSE;/* ... receive data into ring buffer ... */if (ring_buffer_usage(&log_buffer) > 80) {/* Boost log task for 50ms to drain buffer safely from ISR */priority_boost_task_from_isr(log_task_handle, pdMS_TO_TICKS(50), &xHigherPriorityTaskWoken);}portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}
| Scenario | Mechanism |
|---|---|
| General starvation prevention | Aging (periodic task) |
| Buffer near overflow, must drain NOW | Event boost (ISR) |
| Watchdog kick task missed deadline | Event boost (timer callback) |
| Mutex priority inversion | Built-in priority inheritance (mutexes) |
| Periodic task with hard deadline | Rate Monotonic priority assignment + aging |
Here’s a minimal FreeRTOS application demonstrating aging + event boosting:
/* main.c */#include "FreeRTOS.h"#include "task.h"#include "semphr.h"#include "timers.h"/* Configuration */#define MAIN_PRIO (configMAX_PRIORITIES - 2) /* Highest: control loop */#define AGING_PRIO (configMAX_PRIORITIES - 1) /* Aging task must be highest */#define COMMS_PRIO (MAIN_PRIO - 1)#define LOG_PRIO 2#define MONITOR_PRIO 1#define IDLE_PRIO 0#define AGING_INTERVAL_MS 200#define AGING_MAX_BOOST 3/* Aging state */static TaskHandle_t aging_tasks[8];static UBaseType_t aging_orig_prio[8];static uint32_t aging_wait_cycles[8];static uint8_t aging_count = 0;/* Boost state */typedef struct { TaskHandle_t h; UBaseType_t orig; uint32_t left; bool on; } boost_t;static boost_t boosts[4];static uint8_t boost_cnt = 0;/* Register task for aging */void aging_register(TaskHandle_t h) {if (aging_count < 8) {aging_tasks[aging_count] = h;aging_orig_prio[aging_count] = uxTaskPriorityGet(h);aging_wait_cycles[aging_count] = 0;aging_count++;}}/* Request a temporary boost - executed in timer daemon */void execute_boost(void *pv1, uint32_t ticks) {TaskHandle_t h = (TaskHandle_t)pv1;if (boost_cnt >= 4) return;UBaseType_t o = uxTaskPriorityGet(h);UBaseType_t b = o + 2;if (b >= configMAX_PRIORITIES) b = configMAX_PRIORITIES - 1;vTaskPrioritySet(h, b);boosts[boost_cnt].h = h;boosts[boost_cnt].orig = o;boosts[boost_cnt].left = ticks / AGING_INTERVAL_MS;boosts[boost_cnt].on = true;boost_cnt++;}/* Aging and Boost Decay Task */void aging_monitor_task(void *arg) {TickType_t last = xTaskGetTickCount();const TickType_t freq = pdMS_TO_TICKS(AGING_INTERVAL_MS);for (;;) {vTaskDelayUntil(&last, freq);/* Aging */for (uint8_t i = 0; i < aging_count; i++) {TaskHandle_t h = aging_tasks[i];if (!h) continue;eTaskState s = eTaskGetState(h);if (s == eReady) {aging_wait_cycles[i]++;UBaseType_t cur = uxTaskPriorityGet(h);UBaseType_t max = aging_orig_prio[i] + AGING_MAX_BOOST;if (cur < max && cur < configMAX_PRIORITIES - 1) {vTaskPrioritySet(h, cur + 1);}} else {aging_wait_cycles[i] = 0;UBaseType_t cur = uxTaskPriorityGet(h);if (cur > aging_orig_prio[i]) vTaskPrioritySet(h, aging_orig_prio[i]);}}/* Boost decay */for (uint8_t i = 0; i < boost_cnt; i++) {if (boosts[i].on && boosts[i].left > 0) {if (--boosts[i].left == 0) {vTaskPrioritySet(boosts[i].h, boosts[i].orig);boosts[i].on = false;}}}}}/* High-priority control task -- runs 100Hz, tight loop */void control_task(void *arg) {TickType_t last = xTaskGetTickCount();for (;;) {vTaskDelayUntil(&last, pdMS_TO_TICKS(10)); /* 100Hz *//* Simulate heavy computation */for (volatile int i = 0; i < 5000; i++);}}/* Comms task -- periodic, medium priority */void comms_task(void *arg) {TickType_t last = xTaskGetTickCount();for (;;) {vTaskDelayUntil(&last, pdMS_TO_TICKS(50)); /* 20Hz *//* Simulate packet processing */for (volatile int i = 0; i < 2000; i++);}}/* Log task -- low priority, must not starve */void log_task(void *arg) {for (;;) {/* Wait for log entries (simulated) */vTaskDelay(pdMS_TO_TICKS(200));/* Write to flash / UART */for (volatile int i = 0; i < 500; i++);}}/* Monitor task -- lowest, watchdog kick */void monitor_task(void *arg) {for (;;) {vTaskDelay(pdMS_TO_TICKS(1000)); /* 1Hz watchdog kick *//* Kick watchdog */}}/* ISR example: boost log task when buffer full */void EXTI0_IRQHandler(void) {BaseType_t yield = pdFALSE;/* Simulate buffer full condition */xTimerPendFunctionCallFromISR(execute_boost, aging_tasks[0], 50, &yield);portYIELD_FROM_ISR(yield);}int main(void) {/* HW init... */TaskHandle_t h_control, h_comms, h_log, h_monitor, h_aging;xTaskCreate(control_task, "Ctrl", 1024, NULL, MAIN_PRIO, &h_control);xTaskCreate(comms_task, "Comms", 1024, NULL, COMMS_PRIO, &h_comms);xTaskCreate(log_task, "Log", 512, NULL, LOG_PRIO, &h_log);xTaskCreate(monitor_task, "Mon", 512, NULL, MONITOR_PRIO, &h_monitor);xTaskCreate(aging_monitor_task, "Aging", 512, NULL, AGING_PRIO, &h_aging);/* Register low-priority tasks for aging */aging_register(h_log);aging_register(h_monitor);vTaskStartScheduler();for (;;);}
Enable configGENERATE_RUN_TIME_STATS=1 and a high-resolution timer. After running:
char buf[512];vTaskGetRunTimeStats(buf);printf("%s\n", buf);
Expected output showing all tasks get CPU time:
Task Abs Time % Time------------------------------------Ctrl 452310 45%Comms 201230 20%Log 180450 18%Mon 150200 15%IDLE 16000 1%
Without aging, Log and Mon would show 0% or near-0%.
Log priority changes to verify boosting:
/* In aging_monitor_task after vTaskPrioritySet */tracePRINTF("AGING: Task %s prio %u -> %u\r\n", pcTaskGetName(h), old, new);
Output should show gradual increments and restores:
AGING: Task Log prio 2 -> 3AGING: Task Mon prio 1 -> 2AGING: Task Log prio 3 -> 2 (restored after running)
Disable aging, run for 10 seconds -> watchdog fires. Enable aging -> watchdog never fires.
| Concern | Mitigation |
|---|---|
| Priority inversion from boosting | Cap boost levels; don’t boost above critical control tasks |
| Thrashing (frequent boost/restore) | Increase AGING_INTERVAL_MS; add hysteresis (boost +2, restore -1) |
| ISR latency from tick hook work | Keep aging logic out of tick hook; use a dedicated periodic task |
| Starvation of medium-priority tasks | Don’t age tasks above a certain original priority (e.g., only age prio <= 2) |
| Debugging priority chaos | Log every priority change; use configUSE_TRACE_FACILITY + Percepio Tracealyzer |
+----------------------------------------------------------------------+| DECISION FLOW |+----------------------------------------------------------------------+| Is the task missing deadlines because a mutex holder || is preempted by medium-priority tasks? || | || +-- YES -> Use MUTEX + Priority Inheritance (built-in) || | || +-- NO -> Is it general CPU starvation (no mutex)? || | || +-- YES, all low-prio tasks starve -> AGING || | || +-- YES, specific event (buffer full, WD) || -> EVENT-DRIVEN BOOST |+----------------------------------------------------------------------+
tasks.c implementation of vTaskPrioritySet() and uxTaskPriorityGet() (https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/main/tasks.c)Quick Links
Legal Stuff





