FreeRTOS Hooks – Adding Custom Behavior to the Kernel
1. Introduction
When we design a FreeRTOS application, we usually think in terms of tasks: splitting the work into multiple flows, synchronizing them with queues or semaphores, and passing data around. That's the normal way to build an RTOS-based system.
But sometimes we want something different — a place to inject code at key system events without creating another task. For example: run diagnostics when memory runs out, enter low-power mode when the CPU is idle, or log system activity every tick.
This is where a hidden feature of FreeRTOS comes in handy: hooks. Hooks are like callbacks you can implement in your application, and the FreeRTOS kernel will automatically call them when specific events occur. They let you add custom behavior inside the kernel's flow, without touching or modifying FreeRTOS itself.
2. What Are Hooks in FreeRTOS?
Hooks in FreeRTOS are special application functions that act like callbacks into the kernel. Instead of creating a new task or manually checking for conditions, you simply implement a hook, and the kernel will call it at the right moment. This gives you a clean way to add system-level behavior without altering the FreeRTOS source code.
The important thing to understand is that hooks do not run as independent tasks. They execute in the context of existing system tasks or interrupts, which means they have almost no extra cost in terms of memory or scheduling overhead. Because of this, they are extremely efficient. However, it also means that a hook must always be short, non-blocking, and deterministic, since it often runs during critical parts of the kernel.
By enabling the right macros in your FreeRTOSConfig.h file, you can tell the kernel which hooks you want active. Once enabled, the kernel automatically calls them when certain events occur — for example when the system goes idle, when a tick interrupt fires, or when something goes wrong such as a failed memory allocation or a stack overflow. In practice, this turns hooks into powerful extension points that let you blend custom application logic seamlessly with the kernel's normal behavior.

Fig. 1 – FreeRTOS Hooks Concept
3. FreeRTOS Hooks: Extending Kernel Behavior
3.1 Idle Hook
The idle hook runs whenever the scheduler has nothing else to execute. In practice, this happens when no tasks are in the ready state, and the kernel switches to its internal idle task. If you implement this hook, you can take advantage of those spare cycles to do something useful, like putting the MCU into a low-power mode or running very lightweight background jobs. Some developers also call the co-routine scheduler from here, since co-routines are tied to idle time. The main rule is simple: never block or delay inside the idle hook. If you do, you'll interfere with the scheduler itself.
#define configUSE_IDLE_HOOK 1
void vApplicationIdleHook(void)
{
/* Enter sleep until next interrupt */
__WFI();
}
3.2 Tick Hook
The tick hook is executed every time the system tick interrupt fires. Because the tick runs at a fixed frequency, this hook is ideal for small, periodic operations like incrementing a counter, toggling a debug pin, or maintaining a simple software timer. It's important to remember that the tick hook runs inside an interrupt service routine, so the code must be minimal and deterministic. If you need to do anything more complex, the best practice is to signal a task from the hook and let the task handle the work later.
#define configUSE_TICK_HOOK 1
void vApplicationTickHook(void)
{
/* Simple periodic counter */
systemHeartbeatCounter++;
}
3.3 Malloc Failed Hook
Dynamic memory allocation can fail if the heap runs out of space or becomes fragmented. FreeRTOS handles this gracefully by giving you the malloc failed hook. Whenever a call to pvPortMalloc() returns NULL, this hook is triggered. That's your opportunity to log an error, blink an LED, or put the system into a safe recovery mode instead of crashing silently. During development, many people simply halt the processor here so they can catch the problem with a debugger. In production, you would usually prefer to reset the system cleanly or at least record the error for later analysis.
#define configUSE_MALLOC_FAILED_HOOK 1
void vApplicationMallocFailedHook(void)
{
printf("Error: Out of heap memory!\n");
taskDISABLE_INTERRUPTS();
#ifdef DEBUG
/* Trigger breakpoint in debug builds */
__BKPT(0);
/* Stay here if no debugger attached */
for(;;);
#else
/* Reset safely in production */
NVIC_SystemReset();
#endif
}
3.4 Stack Overflow Hook
Stack overflows are one of the most difficult bugs to track in embedded systems, because they can silently corrupt memory. FreeRTOS includes optional stack checking, and if it detects that a task has exceeded its stack, it will call the stack overflow hook. Inside this hook, you can identify which task failed, log the error, or halt the system to prevent further corruption. Some developers even set up distinct LED blink patterns here, so if the device resets in the field, they can immediately see what went wrong. Enabling and implementing this hook is one of the simplest ways to make your FreeRTOS system more robust.
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_STACK_OVERFLOW_HOOK 1
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
/* Print the offending task name for diagnostics */
printf("Stack overflow in task: %s\n", pcTaskName);
/* Stop interrupts to freeze the system */
taskDISABLE_INTERRUPTS();
#ifdef DEBUG
/* Break into debugger and halt execution */
__BKPT(0);
for(;;);
#else
/* Reset system safely in production */
NVIC_SystemReset();
#endif
}
3.5 Daemon Task Startup Hook
When you enable software timers in FreeRTOS, the kernel automatically creates a special background task often called the daemon task or timer service task. Its job is to run all software timer callbacks and handle deferred interrupt work that has been posted to it. You don't create or schedule this task yourself; the kernel manages it for you.
The daemon task startup hook is a function that FreeRTOS calls once, right after this task starts running. It is a perfect place for initialization that needs the scheduler to be active but should happen before the rest of your application tasks get busy. For example, you might use it to bring up a network stack, start logging services, or kick off periodic system checks.
#define configUSE_DAEMON_TASK_STARTUP_HOOK 1
void vApplicationDaemonTaskStartupHook(void)
{
/* Initialize components that depend on the scheduler */
initNetworkStack();
startWatchdogTimer();
}
3.6 Trace Hooks
Unlike the other hooks, trace hooks in FreeRTOS are actually macros inside the kernel. By default, they expand to nothing, but you can redefine them in FreeRTOSConfig.h to capture events like task switches, queue operations, or semaphore usage. This gives you visibility into what the RTOS is doing without touching the kernel source.
#define traceTASK_SWITCHED_IN() myTraceTaskSwitchedIn()
void myTraceTaskSwitchedIn(void)
{
/* Log or timestamp the task that just ran */
printf("Now running: %s\n", pcTaskGetName(NULL));
}
The key is to keep these hooks very lightweight. A common pattern is to log only minimal info inside the macro (like a timestamp or a counter) and let a dedicated system monitor task analyze it later. For example, you might keep a small struct per task with the last time it was scheduled, then have the monitor task check if any task has been "stuck" for too long.
4. Best Practices for Using Hooks
Hooks give you entry points into the kernel, but they also run in sensitive contexts. Keep the code short and predictable. Use them for quick actions like setting flags or recording simple metrics, and delegate heavy work to regular tasks. In development builds, let error hooks halt the system so you can inspect problems. In production, fail gracefully by resetting or moving to a safe state. Finally, only enable the hooks that add real value to your system.
5. Conclusion
FreeRTOS hooks are often overlooked, but they give you a simple way to extend the kernel without touching its source code. By implementing just a few lightweight functions, you can add low-power handling, periodic background jobs, robust error detection, and even custom tracing. The key is to keep them short, safe, and tailored to your system's needs.