
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. Implement it in vApplicationTickHook() — it runs every tick from the ISR context, so keep it fast.
/* FreeRTOSConfig.h additions */#define configUSE_TICK_HOOK 1#define configTICK_RATE_HZ 1000#define AGING_INTERVAL_TICKS 200 /* Boost every 200ms */#define AGING_MAX_PRIORITY_BOOST 3 /* Max +3 priority levels */#define AGING_MIN_PRIORITY 1 /* Don't boost idle *//* Task handle array for aging -- populate at task creation */#define AGING_MAX_TASKS 16static TaskHandle_t aging_task_handles[AGING_MAX_TASKS];static uint8_t aging_task_count = 0;static uint32_t aging_tick_counters[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_tick_counters[aging_task_count] = 0;aging_original_priorities[aging_task_count] = uxTaskPriorityGet(task);aging_task_count++;}}/* Tick hook -- runs in ISR context, keep minimal */void vApplicationTickHook(void) {/* Increment wait counters for all registered tasks */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);/* Only age tasks that are READY but not RUNNING */if (state == eReady) {aging_tick_counters[i]++;if (aging_tick_counters[i] >= AGING_INTERVAL_TICKS) {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 */vTaskPrioritySet(h, curr_prio + 1);aging_tick_counters[i] = 0; /* Reset counter */}}} else {/* Task ran or blocked -- reset counter, restore original priority if boosted */if (aging_tick_counters[i] > 0) {aging_tick_counters[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()eReady state (ready but not running)AGING_INTERVAL_TICKS), boost priority by 1 levelAGING_MAX_PRIORITY_BOOST levels above original — never exceed configMAX_PRIORITIES - 1| Parameter | Typical Value | Rationale |
|---|---|---|
AGING_INTERVAL_TICKS | 100-500 (at 1kHz) | 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.
/* 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;/* Call from ISR or task to boost a task for N ticks */BaseType_t priority_boost_task(TaskHandle_t task, uint32_t duration_ticks) {if (boost_count >= 8) return pdFALSE;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] = (priority_boost_t){.task = task,.original_priority = orig,.boost_ticks = duration_ticks,.active = true};boost_count++;return pdTRUE;}/* Call from tick hook or timer callback 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) {/* ... receive data into ring buffer ... */if (ring_buffer_usage(&log_buffer) > 80) {/* Boost log task for 50ms to drain buffer */priority_boost_task(log_task_handle, 50); /* 50 ticks at 1kHz */}}
| Scenario | Mechanism |
|---|---|
| General starvation prevention | Aging (tick hook) |
| 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 - 1) /* Highest: control loop */#define COMMS_PRIO (MAIN_PRIO - 1)#define LOG_PRIO 2#define MONITOR_PRIO 1#define IDLE_PRIO 0#define AGING_INTERVAL_TICKS 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_ticks[8];static uint8_t aging_count = 0;/* Boost state */static struct { TaskHandle_t h; UBaseType_t orig; uint32_t left; bool on; } 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_ticks[aging_count] = 0;aging_count++;}}/* Tick hook: aging + boost decay */void vApplicationTickHook(void) {/* 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) {if (++aging_wait_ticks[i] >= AGING_INTERVAL_TICKS) {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);aging_wait_ticks[i] = 0;}}} else {aging_wait_ticks[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;}}}}/* Request a temporary boost */void request_boost(TaskHandle_t h, uint32_t ticks) {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++] = (typeof(boosts[0])){h, o, ticks, true};}/* 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 */request_boost(aging_tasks[0], 50); /* Boost log task for 50ms */portYIELD_FROM_ISR(yield);}int main(void) {/* HW init... */TaskHandle_t h_control, h_comms, h_log, h_monitor;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);/* Register low-priority tasks for aging */aging_register(h_log);aging_register(h_monitor);/* Don't register control/comms -- they should run at fixed priority */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 vApplicationTickHook 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_TICKS; add hysteresis (boost +2, restore -1) |
| ISR latency from tick hook work | Keep hook < 5uss; move complex logic to a timer callback 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





