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 the Tick Hook
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 the Tick Hook

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

How It Works

  1. Register each task you want aged after creation with aging_register_task()
  2. Every tick, the hook increments a wait counter for tasks in eReady state (ready but not running)
  3. When counter reaches threshold (AGING_INTERVAL_TICKS), boost priority by 1 level
  4. When task runs or blocks, reset counter and restore original priority
  5. Cap boost at AGING_MAX_PRIORITY_BOOST levels above original — never exceed configMAX_PRIORITIES - 1

Tuning Parameters

ParameterTypical ValueRationale
AGING_INTERVAL_TICKS100-500 (at 1kHz)Lower = 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.

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

When to Use Each

ScenarioMechanism
General starvation preventionAging (tick hook)
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 - 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 (;;);
}

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 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 -> 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_TICKS; add hysteresis (boost +2, restore -1)
ISR latency from tick hook workKeep hook < 5uss; move complex logic to a timer callback 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 tick hook) 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 Queue Overrun Data Loss in ISR Contexts
Fixing FreeRTOS Queue Overrun Data Loss in ISR Contexts
August 19, 2026
4 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media