Task priorities are one of the first FreeRTOS concepts that seem simple at a glance but become much more important once you start building real applications. At the most basic level, a priority tells the scheduler which task should run first when more than one task is ready to execute. But in practice, priorities shape the entire behavior of a system. They affect responsiveness, timing, CPU usage, fairness, and even whether some tasks get enough time to run at all.
Beginners often assume priorities are just a way to label tasks as “important” or “not important.” That is only partly true. A priority is not a human description. It is a scheduling decision. If two ready tasks compete for CPU time, the scheduler uses priority to decide which one gets to run. That means a poor priority choice can make a system feel unstable even when the code itself is otherwise correct.
This article explains task priorities in a practical way, using clear examples to show how they affect real FreeRTOS behavior. The goal is not just to define priority levels, but to help you understand how to choose them sensibly.
What a task priority means
In FreeRTOS, each task has a numeric priority. Higher numbers represent higher priorities. When several tasks are ready to run, the scheduler chooses the highest-priority ready task.
That one rule explains a lot of RTOS behavior.
If a task with priority 3 is ready and a task with priority 1 is also ready, the priority 3 task runs first. The lower-priority task only runs when the higher-priority task blocks, delays, suspends itself, or otherwise stops being ready.
This means priority is directly tied to CPU access. A higher-priority task does not just get “more attention.” It can completely prevent a lower-priority task from running if it stays ready all the time.
That is why priorities must be assigned with care.
Ready, blocked, and running
To understand priorities, you also need to understand task states.
A task that is ready can run if the scheduler selects it.
A task that is blocked is waiting for something, such as time to pass, data to arrive in a queue, or a semaphore to become available.
A task that is running is the task currently using the CPU.
Priorities only matter among tasks that are ready. A blocked high-priority task does not compete with a ready low-priority task because the blocked task is not eligible to run yet.
This is an important point because many beginner misunderstandings come from imagining that higher-priority tasks always dominate. They dominate only when they are ready.
A very simple example
Imagine a system with two tasks:
LedTaskat priority 1SerialTaskat priority 2
If both tasks are ready at the same time, SerialTask runs first because it has the higher priority.
A simple version might look like this:
void LedTask(void *pvParameters)
{
for (;;)
{
toggle_led();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void SerialTask(void *pvParameters)
{
for (;;)
{
printf("Status message\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
In this example, both tasks spend most of their time blocked by vTaskDelay(). That means both get chances to run. The scheduler wakes them when their delays expire, and if both happen to become ready together, SerialTask will run first.
This is a healthy pattern. Each task does a bit of work, then blocks. Priorities matter, but they do not cause starvation because the tasks are cooperative in the sense that they regularly stop being ready.
Example: why a high-priority task can starve lower ones
Now consider a more dangerous design:
void HighTask(void *pvParameters)
{
for (;;)
{
process_data();
}
}
void LowTask(void *pvParameters)
{
for (;;)
{
toggle_led();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
Suppose HighTask has priority 3 and LowTask has priority 1.
HighTask never blocks, never delays, and never waits. It is always ready. That means the scheduler keeps choosing it. LowTask may never run at all.
This is one of the most important real lessons about priorities: a high-priority task that stays ready continuously can starve lower-priority tasks.
Beginners sometimes see this and think the scheduler is broken. It is not. It is doing exactly what the priorities say.
Equal-priority tasks
If two tasks have the same priority and both are ready, they can share CPU time according to the scheduler’s rules. In many FreeRTOS setups, equal-priority tasks can take turns through time slicing.
For example:
void TaskA(void *pvParameters)
{
for (;;)
{
printf("A\n");
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void TaskB(void *pvParameters)
{
for (;;)
{
printf("B\n");
vTaskDelay(pdMS_TO_TICKS(100));
}
}
If both have the same priority, neither one automatically dominates the other just because of priority. Their timing and scheduler behavior determine how they alternate.
This is useful when tasks are of similar importance and you want them treated as peers.
But equal priority does not always mean equal real behavior. If one task blocks often and another stays ready longer, the task behavior will still differ. The scheduler can only choose among tasks that are actually ready.
Real example: sensor task, display task, and logger task
Consider a small embedded device with three jobs:
- read a sensor every 50 milliseconds
- update a display every 250 milliseconds
- write debug logs over UART every 1 second
A sensible priority assignment might be:
SensorTaskpriority 3DisplayTaskpriority 2LoggerTaskpriority 1
Why?
The sensor task is most time-sensitive. If it is delayed too much, measurements may become inconsistent.
The display task matters, but a slight delay is usually acceptable.
The logger task is least important. If logs are delayed slightly, the system still works.
A sketch could look like this:
void SensorTask(void *pvParameters)
{
for (;;)
{
read_sensor();
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void DisplayTask(void *pvParameters)
{
for (;;)
{
update_display();
vTaskDelay(pdMS_TO_TICKS(250));
}
}
void LoggerTask(void *pvParameters)
{
for (;;)
{
write_log();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
This is a good real-world example because it shows how priorities often reflect timing sensitivity, not just vague importance.
Real example: button response versus background processing
Imagine another system with two main functions:
- respond immediately to a button press
- calculate statistics in the background
You might use:
ButtonTaskpriority 4StatsTaskpriority 1
Why?
If the button event must feel responsive to the user, the task handling it should be able to run quickly once the event occurs. Background statistics work can happen whenever CPU time is available.
Example:
void ButtonTask(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(buttonSemaphore, portMAX_DELAY) == pdTRUE)
{
handle_button_event();
}
}
}
void StatsTask(void *pvParameters)
{
for (;;)
{
calculate_statistics();
vTaskDelay(pdMS_TO_TICKS(200));
}
}
This works well because ButtonTask is blocked most of the time, waiting on a semaphore. It only runs when needed, and when it does become ready, its higher priority allows a fast reaction.
That is a good use of high priority.
High priority should not mean heavy continuous work
A common mistake is assigning high priority to a large computational task because it feels “important.”
For example, suppose a developer writes:
void DataProcessingTask(void *pvParameters)
{
for (;;)
{
run_large_algorithm();
}
}
and gives it priority 5 because the algorithm seems critical.
This may be a bad design if the task never blocks. It can interfere with everything below it.
A better design might be:
- keep the task at a more moderate priority
- break the work into chunks
- block on input data
- yield CPU time naturally through queues, delays, or event waiting
In FreeRTOS, a task should often be high priority because it must respond quickly, not because it performs a lot of work.
That is an important distinction.
Priorities and blocking behavior work together
Priorities alone do not define a system. Blocking behavior matters just as much.
A low-priority task that is always ready may still use plenty of CPU if all higher-priority tasks spend most of their time blocked.
A high-priority task that only wakes briefly once per second may have very little overall CPU impact.
This means good RTOS design usually combines:
- sensible priorities
- tasks that block when idle
- communication mechanisms like queues and semaphores
- short bursts of work instead of endless busy loops
A system with well-chosen priorities but poor blocking behavior can still perform badly.
Example: queue-based design with priorities
Suppose you have a UART receive interrupt feeding bytes into a queue, and a task processes full commands. You also have a status LED task.
A sensible design might be:
CommandTaskpriority 3LedTaskpriority 1
void CommandTask(void *pvParameters)
{
char cmd;
for (;;)
{
if (xQueueReceive(commandQueue, &cmd, portMAX_DELAY) == pdTRUE)
{
process_command(cmd);
}
}
}
void LedTask(void *pvParameters)
{
for (;;)
{
toggle_led();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
This is a nice example because the higher-priority command task is not wasteful. It blocks until data arrives. When a command comes in, it wakes quickly and handles it. The LED task fills spare time.
That is exactly the kind of behavior FreeRTOS priorities are good at supporting.
Priority inversion: an important real-world issue
Task priorities become more complicated when tasks share resources.
Imagine:
HighTaskpriority 3MediumTaskpriority 2LowTaskpriority 1
Now suppose LowTask holds a mutex protecting a shared UART. While it holds the mutex, HighTask becomes ready and tries to take the same mutex. It cannot, so it blocks.
At this moment, you might expect LowTask to run soon, finish its UART work, and release the mutex. But if MediumTask is ready and does not need that mutex, it can run before LowTask because it has higher priority than LowTask.
Now the high-priority task is effectively delayed by a low-priority task, with a medium-priority task making the delay worse.
That is priority inversion.
This is one reason mutexes are important in RTOS systems. Proper mutex behavior helps reduce this problem.
Even beginners should know this term because it explains why priorities alone do not solve everything.
Example: bad priority assignment in a user interface project
Suppose you build a device with these tasks:
TouchTaskScreenTaskStorageTask
A beginner might assign:
StorageTaskpriority 4ScreenTaskpriority 2TouchTaskpriority 1
because saving data feels very important.
But this may create a poor user experience. If storage work keeps preempting the user interface tasks, touches may feel laggy and the screen may update unevenly.
A better choice might be:
TouchTaskpriority 4ScreenTaskpriority 3StorageTaskpriority 1 or 2
Why?
Because user interaction is time-sensitive. Storage is important, but often not urgent in the same immediate way.
This shows an important principle: priority should reflect urgency of response, not emotional importance.
Example: motor control versus telemetry
Consider a robotics system with:
MotorControlTaskTelemetryTaskDebugPrintTask
A reasonable priority pattern might be:
MotorControlTaskpriority 5TelemetryTaskpriority 3DebugPrintTaskpriority 1
The motor control loop affects physical behavior and may need tight timing.
Telemetry matters, but a small delay is acceptable.
Debug printing should never interfere with control.
This is a classic embedded priority structure. Control first, communication second, diagnostics last.
Why too many priority levels can be a problem
Beginners sometimes think every task needs a unique priority. That often leads to overcomplicated systems.
For example, assigning priorities 1, 2, 3, 4, 5, 6, and 7 to seven tasks may create a design that is harder to reason about than necessary.
In many systems, it is better to think in a few broad classes:
- urgent real-time tasks
- normal application tasks
- background or maintenance tasks
That might mean several tasks share the same priority rather than each having their own special number.
This can make the design easier to understand and maintain.
A good practical way to choose priorities
A useful practical method is to ask these questions for each task:
How quickly must this task respond once it becomes ready?
What happens if this task is delayed?
Does this task spend most of its time blocked?
Does this task protect or affect time-sensitive hardware?
Is this task background work that can wait?
Tasks with strong timing requirements or urgent event handling usually deserve higher priorities.
Tasks doing periodic but less urgent work often fit in the middle.
Tasks for logging, diagnostics, statistics, or maintenance usually belong near the bottom.
This is much more reliable than assigning priorities based on guesswork.
Example of a balanced small system
Here is a simple balanced system:
ControlTaskpriority 4CommsTaskpriority 3UiTaskpriority 2LogTaskpriority 1
void ControlTask(void *pvParameters)
{
for (;;)
{
run_control_loop();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void CommsTask(void *pvParameters)
{
for (;;)
{
if (xQueueReceive(commsQueue, &msg, portMAX_DELAY) == pdTRUE)
{
handle_message(msg);
}
}
}
void UiTask(void *pvParameters)
{
for (;;)
{
update_ui();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void LogTask(void *pvParameters)
{
for (;;)
{
flush_logs();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
This is a realistic pattern. The fastest control loop gets top priority. Communications stay responsive. UI updates remain regular. Logging stays at the bottom.
The system is structured by urgency, not by arbitrary numbering.
Common beginner mistakes
One common mistake is giving a task high priority just because it seems important.
Another is creating a high-priority task that never blocks, starving the rest of the system.
Another is assuming low-priority tasks will always get time eventually. They will not if higher-priority ready tasks never stop running.
Another is using too many distinct priority levels without a clear reason.
Another is forgetting that blocking is healthy in RTOS design. A task waiting on a queue or semaphore is often behaving correctly, not “doing nothing.”
A final common mistake is trying to fix a design problem by only changing priorities. Sometimes the real issue is poor task structure, busy waiting, or bad resource sharing.
A simple mental model
A useful mental model is this:
- Priority answers the question, “If several tasks are ready right now, which one should run first?”
- It does not answer every scheduling question.
- It does not replace good blocking design.
- It does not automatically make code fast or efficient.
- It does not solve shared resource problems by itself.
- But it is one of the main tools that shapes responsiveness in an RTOS system.
Conclusion
Task priorities in FreeRTOS are not just labels. They are scheduling rules that determine which ready task gets CPU time first. A higher-priority ready task runs before a lower-priority ready task, which means bad priority choices can lead to starvation, poor responsiveness, or unstable-feeling behavior.
The best way to assign priorities is to think in terms of urgency and timing sensitivity. Tasks that must respond quickly or support time-critical behavior usually deserve higher priorities. Tasks doing less urgent work should be lower. Background logging and maintenance should generally stay near the bottom.

