Skip to main content Why RTOS tick, Software Timers, and Timeouts Are Not Real Time | IoT Worker

Why RTOS tick, Software Timers, and Timeouts Are Not Real Time

RTOS code often contains calls like:

vTaskDelay(1);
xQueueReceive(q, &msg, 10);
xTimerStart(timer, 0);

They all look like “wait for some time”. But RTOS time is usually not continuous, precise, and immediately executed. Many failures come from reading tick, delay, timeout, and software timers as precise real-time behavior.

A safer model is:

hardware clock generates tick
-> RTOS updates tick count
-> expired tasks or timers become ready/pending
-> scheduler decides when code actually runs
-> low power and interrupt masking change this path

So delay 10 ms is closer to “do not run until at least a tick boundary and then wait for scheduling” than “this code will run exactly 10 ms later”.

tick Is the RTOS Time Granularity

Many RTOSes use a periodic tick interrupt to advance system time: 1 ms per tick, 10 ms per tick, or another interval.

tick affects:

  • task delays
  • timeouts
  • software timers
  • time slicing
  • low-power wake decisions

If the system tick is 10 ms, a 1 ms delay request cannot reliably sleep for real 1 ms. It is quantized to tick boundaries and then affected by scheduling latency.

tick: 0ms  10ms  20ms  30ms
task calls delay(1 tick) at 9ms
it may become ready around 10ms
actual execution still depends on the scheduler

Many RTOS delay parameters are in ticks, not milliseconds. Confusing milliseconds with ticks creates subtle bugs.

delay(1) Is Not Precise Timing

delay(1) is often misused as “wait 1 ms” or “let hardware settle”.

It may actually mean:

  • delay for one tick
  • yield until the next tick
  • wait until the scheduler runs this task again

None of these is precise busy-wait timing.

If the tick is 10 ms, delay(1) is a 10 ms-scale wait. If the call happens near a tick boundary, the actual wait also varies.

For microsecond or sub-tick waits, task delay is usually the wrong tool. Consider:

  • hardware timers
  • short bounded polling of peripheral status
  • capture/compare
  • DMA or peripheral completion interrupts
  • dedicated high-resolution counters

Task delay is for scheduler-level waiting, not waveform timing, protocol bit timing, or precise hardware setup time.

timeout Is a Maximum Wait Boundary

A wait with timeout is not just sleep.

For example:

xQueueReceive(q, &msg, timeout);

It means:

if a message arrives first, return early
if no message arrives, return timeout after the timeout boundary

timeout is the maximum wait boundary, not a guarantee to sleep the whole time.

Also, timeout expiry only means the task can leave the wait queue. Actual execution still depends on priorities, interrupt-disabled time, critical sections, CPU load, and higher-priority tasks.

So “10 tick timeout” means “around 10 ticks, the timeout condition can be satisfied”, not “the next line executes exactly after 10 ticks”.

Software Timers Are Not Thread Pools

In many RTOSes, software timer callbacks do not run in the task that created the timer. They run in a timer service task, daemon task, softirq-like context, or similar mechanism.

This creates important boundaries:

  • callbacks must not run for long
  • callbacks should not perform blocking I/O
  • callbacks should not wait on objects that depend on the same timer service
  • heavy logging in callbacks can delay other timers
  • one stuck callback can affect all timers served by that context

Software timers fit lightweight work:

  • set a state bit
  • post a message
  • notify a task
  • advance a state machine

Heavy work belongs in normal tasks.

Periodic Timers Accumulate Jitter

Periodic tasks are often written in two styles:

delay(period) after each execution
wait until the next absolute period boundary

The first style includes execution time in the period:

run 3ms
delay 10ms
next interval is about 13ms plus scheduling latency

The second style is better for fixed frequency because it uses target time points.

If the RTOS provides a “delay until” style API, periodic tasks should usually prefer it over simply delaying a relative amount every loop.

Otherwise execution time, scheduling latency, logs, and higher-priority tasks cause drift.

tickless idle Changes Tick Intuition

To save power, many RTOSes support tickless idle. When the system is idle, it does not wake the CPU on every tick. It computes the next required wake time and enters deeper sleep.

This changes several assumptions:

  • tick interrupts are no longer continuous and equally spaced
  • sleep may be timed by a low-power timer or RTC
  • after wake, the RTOS must compensate for elapsed ticks
  • compensation accuracy depends on the low-power clock
  • entering and leaving deep sleep has overhead

If the low-power clock is inaccurate, or compensation is wrong, task delays, timeouts, and software timers drift.

Debugging “bad timeouts” in low-power systems requires checking which clock runs during sleep, not only the RTOS tick configuration.

Interrupt Masking and Critical Sections Delay tick

tick usually comes from a timer interrupt. If the system masks interrupts for too long, tick handling is delayed.

Results include:

  • delay appears longer
  • timeout returns late
  • software timers expire in a burst
  • scheduling latency grows
  • watchdog heartbeat tasks jitter

Long critical sections, high-priority ISRs, scheduler locks, and interrupt masking in drivers can all prevent “time has arrived” from being processed promptly.

For RTOS timing issues, check:

  • whether tick interrupts arrive on time
  • maximum interrupt-disabled time
  • whether high-priority tasks monopolize CPU
  • whether the timer service task has enough priority
  • whether software timer callbacks are too heavy

timeout Should Not Replace Real Readiness

Many field issues come from using timeout to hide unclear state design:

wait 100ms and assume device is ready
wait 5 ticks and assume module init is done
wait 1 second and assume network recovered

This turns real dependencies into guessed time. Load, low power, tickless idle, slow peripherals, and retry paths break the assumption.

Prefer:

  • wait for explicit events
  • wait for status bits
  • wait for queue messages
  • wait for hardware interrupts
  • wait for condition combinations
  • use timeout only as the failure boundary

timeout should answer “what if it never happens”, not replace “what are we waiting for”.

Debugging Order

For RTOS problems such as inaccurate timing, long delays, jittery software timers, or low-power timeout failures, inspect:

tick period
-> whether API parameters are ticks or ms
-> relative delay or absolute periodic wait
-> timeout waiting for event or guessed time
-> software timer callback context
-> whether timer service task is blocked
-> long interrupt or scheduler masking
-> whether tickless idle is enabled
-> which low-power clock runs during sleep
-> whether tick compensation after wake is correct

If the issue appears only under low power, verbose logging, high load, or high interrupt rate, focus on scheduling latency, interrupt-disabled time, and timer callback context.

RTOS Time Is a System Path

RTOS time is not just a number. It goes through hardware clocks, ticks, kernel counters, wait queues, software timers, the scheduler, and power management.

vTaskDelay(1), software timers, and timeouts all depend on that path. They express scheduler-level waiting and failure boundaries, but they do not automatically provide precise real time.

Separate tick granularity, scheduling latency, low-power compensation, and callback context, and many “bad timing” failures become measurable system behavior.