HomeAbout UsContact Us

Fixing FreeRTOS Task Starvation: Priority Boosting & Aging

By Jithin Tom
Published in Embedded OS
August 22, 2026
3 min read
Fixing FreeRTOS Task Starvation: Priority Boosting & Aging

Table Of Contents

01
Root Cause: Fixed-Priority Preemption Without Bounds
02
Why Priority Inheritance Doesn't Fix Starvation
03
Solution 1: Priority Aging in a Periodic Task
04
Solution 2: Event-Driven Priority Boosting
05
Solution 3: Complete Working Example
06
Verification: Prove It Works
07
Trade-offs and Pitfalls
08
When to Use What
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

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.

Root Cause: Fixed-Priority Preemption Without Bounds

+------------------------------------------------------------------+
| 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:

  • A priority-5 task running a tight loop or frequent periodic work blocks everything below it
  • Priority-0 (idle) never runs -> vApplicationIdleHook() never fires -> no background cleanup
  • Watchdog kick from a low-priority task stops -> system reset
  • Log buffers overflow, network stacks stall, flash wear-leveling never triggers

The scheduler has no built-in mechanism to say “this task has waited long enough, let it run.”

Why Priority Inheritance Doesn’t Fix Starvation

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
|
v
Task L inherits Task H's priority
|
v
Task L runs, releases mutex
|
v
Task 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:

  • High-priority tasks simply never block (tight loops, short periods)
  • No mutex is involved — just pure CPU competition
  • Medium-priority tasks preempt the boosted low-priority task before it releases the mutex

Starvation is a system-wide CPU allocation problem, not a mutex-specific inversion problem. You need both mechanisms.

Solution 1: Priority Aging in a Periodic Task

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);
}
}
}
}
}

How It Works

  1. Register each task you want aged after creation with aging_register_task()
  2. The Aging Task wakes up every AGING_INTERVAL_MS (e.g., 200ms). It must run at a very high priority so it doesn’t get starved itself.
  3. It checks task states. If a registered task is in eReady state, it increments its priority.
  4. When a task runs or blocks, its state is no longer eReady during the next check, so its priority is restored.
  5. Cap boost at AGING_MAX_PRIORITY_BOOST levels above original — never exceed configMAX_PRIORITIES - 1.

Tuning Parameters

ParameterTypical ValueRationale
AGING_INTERVAL_MS100-500msLower = faster response for starved tasks, higher = less priority thrashing
AGING_MAX_PRIORITY_BOOST2-4Enough to break through medium-priority noise, not enough to starve others
configMAX_PRIORITIES>= 7Need headroom above your highest app priority for boosts to work

Task Registration Example

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 */
}

Solution 2: Event-Driven Priority Boosting

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);
}

When to Use Each

ScenarioMechanism
General starvation preventionAging (periodic task)
Buffer near overflow, must drain NOWEvent boost (ISR)
Watchdog kick task missed deadlineEvent boost (timer callback)
Mutex priority inversionBuilt-in priority inheritance (mutexes)
Periodic task with hard deadlineRate Monotonic priority assignment + aging

Solution 3: Complete Working Example

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 (;;);
}

Verification: Prove It Works

1. Runtime Stats

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%.

2. Priority Trace

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 -> 3
AGING: Task Mon prio 1 -> 2
AGING: Task Log prio 3 -> 2 (restored after running)

3. Watchdog Test

Disable aging, run for 10 seconds -> watchdog fires. Enable aging -> watchdog never fires.

Trade-offs and Pitfalls

ConcernMitigation
Priority inversion from boostingCap 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 workKeep aging logic out of tick hook; use a dedicated periodic task
Starvation of medium-priority tasksDon’t age tasks above a certain original priority (e.g., only age prio <= 2)
Debugging priority chaosLog every priority change; use configUSE_TRACE_FACILITY + Percepio Tracealyzer

When to Use What

+----------------------------------------------------------------------+
| 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 |
+----------------------------------------------------------------------+

Summary

  • Starvation is inherent in fixed-priority preemptive schedulers — not a bug, a design consequence
  • Priority inheritance (mutexes) solves inversion, not starvation
  • Aging (time-based, in a periodic task) ensures every registered task eventually runs
  • Event boosting (ISR/timer-driven) handles urgent “must run now” cases
  • Combine both: aging for baseline fairness, boosting for deadline-critical moments
  • Verify with runtime stats — every task should show non-zero CPU percentage

References

  1. FreeRTOS Kernel Developer Guide — “Task Priorities and Preemption” (https://www.freertos.org/Documentation/02-Kernel/04-API-references/02-Task-control/00-Task-control)
  2. FreeRTOS Reference Manual — “Mutexes and Priority Inheritance” (https://freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/04-Mutexes)
  3. Liu, C. L., & Layland, J. W. (1973). “Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment.” Journal of the ACM, 20(1), 46-61.
  4. Sha, L., Rajkumar, R., & Lehoczky, J. P. (1990). “Priority Inheritance Protocols: An Approach to Real-Time Synchronization.” IEEE Transactions on Computers, 39(9), 1175-1185.
  5. Burns, A., & Wellings, A. (2009). Real-Time Systems and Programming Languages. Addison-Wesley. Chapter 7: Fixed Priority Scheduling.
  6. FreeRTOS Source — tasks.c implementation of vTaskPrioritySet() and uxTaskPriorityGet() (https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/main/tasks.c)

Frequently Asked Questions

What causes task starvation in FreeRTOS?

Task starvation occurs when higher-priority tasks continuously preempt lower-priority tasks, preventing them from ever executing. This is common in priority-based preemptive schedulers when the system is overloaded or when priority inversion is not properly handled.

How does priority aging prevent starvation?

Priority aging gradually increases the effective priority of tasks that have been waiting long without executing. This ensures that even the lowest-priority task will eventually get CPU time, preventing indefinite postponement.

What is the difference between priority boosting and priority aging?

Priority boosting temporarily elevates a task's priority in response to a specific event (e.g., mutex ownership for priority inheritance). Priority aging is a systematic, time-based mechanism that gradually raises priorities of long-waiting tasks regardless of specific events.

Can FreeRTOS built-in priority inheritance solve starvation?

Priority inheritance (configUSE_MUTEXES=1 with mutexes) solves priority inversion for mutex-protected critical sections, but it does not prevent general starvation of low-priority tasks when high-priority tasks monopolize the CPU. Aging or explicit boosting is still needed.

What is a practical aging interval for a 1kHz tick system?

A common starting point is aging every 100-500 ticks (100-500ms at 1kHz). The interval should be tuned so that the lowest-priority task gets a priority boost before its response-time deadline expires. Monitor with runtime stats to validate.

Tags

freertostask-starvationpriority-boostingagingpriority-inversionscheduling

Share


Previous Article
Fixing UART DMA Overrun Errors on STM32
Jithin Tom

Jithin Tom

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

Related Posts

Fixing FreeRTOS Event Group Timer Queue Overflow in ISR Context
Fixing FreeRTOS Event Group Timer Queue Overflow in ISR Context
September 07, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media