← Back to Blog

Understanding FreeRTOS Task Priorities and Starvation

If you've worked with FreeRTOS for any length of time, you've likely hit a situation where a low-priority task simply stops running. The system isn't frozen — your high-priority tasks are humming along — but something in the background has silently starved. Let's unpack why this happens and how to prevent it.

How the Scheduler Works

FreeRTOS uses a preemptive, priority-based scheduler by default. The highest-priority ready task always runs. If two tasks share the same priority, they round-robin with each tick. Simple enough — but the implications catch people off guard.

// Two tasks at different priorities
xTaskCreate(vHighTask, "High", 256, NULL, 3, NULL);
xTaskCreate(vLowTask,  "Low",  256, NULL, 1, NULL);

void vHighTask(void *pv) {
    for (;;) {
        // If this never blocks, vLowTask never runs
        processData();
    }
}

If vHighTask never yields — no vTaskDelay(), no semaphore wait, no queue receive with a timeout — then vLowTask is permanently starved.

Priority Inversion

The classic scenario: Task H (high priority) waits on a mutex held by Task L (low priority). Meanwhile, Task M (medium priority) preempts Task L. Now Task H is blocked waiting for Task L, which is blocked by Task M. The effective priority is inverted.

FreeRTOS offers priority inheritance mutexes via xSemaphoreCreateMutex(). When Task H blocks on a mutex held by Task L, the scheduler temporarily boosts Task L to Task H's priority.

Practical Strategies

First, every task's main loop should include at least one blocking call. Even a vTaskDelay(1) is enough to yield the processor.

void vSensorTask(void *pv) {
    TickType_t xLastWake = xTaskGetTickCount();
    for (;;) {
        readSensor();
        // Guaranteed yield point every 100ms
        vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(100));
    }
}

Second, use configUSE_PREEMPTION and configUSE_TIME_SLICING intentionally. If you disable time slicing, equal-priority tasks won't round-robin.

Third, instrument your system. FreeRTOS's runtime stats (vTaskGetRunTimeStats()) will show you exactly how much CPU time each task is consuming.

Takeaway

The scheduler does exactly what you tell it to. Starvation is a design problem, not a bug. Build every task around explicit blocking points, use priority inheritance mutexes, and monitor runtime stats.