How to Implement Watchdog Patterns for Field Reliability

Engineers debugging watchdog timer circuits on embedded system hardware in field conditions

Your device shipped with a watchdog timer enabled. Six months later, a unit in the field locked up and stayed locked up for three days, until someone physically power-cycled it. The watchdog was running the entire time. It just never fired, because your firmware was dutifully kicking it on every pass through main(), regardless of whether the system was actually doing its job.

This is the most common watchdog failure mode in production firmware, and it’s entirely self-inflicted. The watchdog didn’t fail. The feeding strategy did.

This article isn’t about what a watchdog is. It’s about how to structure your watchdog so it catches the failures that matter: hung tasks, deadlocked resources, runaway loops. We’ll walk through three patterns (task check-in, windowed feeding, and sequence validation) with code you can adapt to bare-metal or RTOS projects. Watchdogs address one specific failure class: software hangs and deadlocks. A complete firmware reliability strategy also needs brown-out detection, safe boot design, CRC integrity checks, and communications heartbeats. But getting the watchdog layer right is table stakes, and most teams get it wrong.

Watchdog Timer Fundamentals

A quick refresher so we’re using the same vocabulary.

A hardware watchdog timer (HW WDT) is a peripheral with a countdown counter. Your firmware periodically resets (“kicks” or “feeds”) the counter. If the counter hits zero, the hardware forces a system reset. Because it’s implemented in hardware, it works even if the CPU is stuck in a hard fault or an infinite loop, making it your last line of defense. A software watchdog is implemented in firmware (typically a timer ISR). It’s useful for monitoring individual tasks but can’t save you if the scheduler itself hangs. Always use a hardware WDT as the outermost safety net.

A windowed watchdog (WWDT) adds a twist: the feed must arrive within a specific time window, not too late, but also not too early. This catches a class of failures that standard watchdogs miss entirely (more on this in Pattern 2).

Timeout selection is a tradeoff. The timeout must be longer than the worst-case legitimate execution path, but short enough that downtime after a real hang is acceptable. If your slowest periodic task takes 500ms, a 2-second WDT timeout gives headroom. A 30-second timeout means the system sits dead for up to 30 seconds before recovery. Pick based on your product’s tolerance for downtime.

The Anti-Pattern: Unconditional Feeding

Here’s the code that ships in more products than anyone wants to admit:

int main(void) {
    system_init();
    wdt_enable(WDT_TIMEOUT_2S);

    while (1) {
        read_sensors();
        process_data();
        update_display();
        send_telemetry();

        wdt_feed();  /* Kick the dog every loop iteration */
    }
}

This looks reasonable. It isn’t.

If send_telemetry() deadlocks on a socket that never connects, the while(1) loop blocks, and the watchdog fires. That works. But if process_data() silently fails (returns immediately because a queue is empty), the loop keeps spinning, wdt_feed() keeps executing, and the watchdog stays happy while your system produces garbage output. The device is half-dead but the watchdog doesn’t know.

The core principle: a watchdog feed should only execute when the system has proven it is healthy. This is the “proof of life” concept. Every pattern below is a variation on enforcing that proof.

Pattern 1: Task Check-In With a Single Supervisor

This is the workhorse pattern for multi-task systems and the single biggest improvement most projects can make.

Concept: Each task or module must “check in” by a deadline. A single supervisor, either a dedicated task or a timer ISR, inspects all check-ins. Only if every task has reported in does the supervisor feed the hardware WDT.

Here’s a clean, platform-agnostic implementation:

#include <stdint.h>
#include <stdbool.h>

#define MAX_WATCHED_TASKS 8

typedef struct {
    bool checked_in[MAX_WATCHED_TASKS];
    uint8_t num_tasks;
} watchdog_supervisor_t;

static watchdog_supervisor_t wdt_sup = { .num_tasks = 0 };

/* Each task calls this after completing its critical work */
void wdt_task_checkin(uint8_t task_id) {
    if (task_id < wdt_sup.num_tasks) {
        wdt_sup.checked_in[task_id] = true;
    }
}

/* Register a task at init time; returns assigned task ID */
uint8_t wdt_register_task(void) {
    uint8_t id = wdt_sup.num_tasks;
    wdt_sup.checked_in[id] = false;
    wdt_sup.num_tasks++;
    return id;
}

/* Supervisor calls this periodically (e.g., from a timer ISR or
   a dedicated low-priority task) */
void wdt_supervisor_evaluate(void) {
    for (uint8_t i = 0; i < wdt_sup.num_tasks; i++) {
        if (!wdt_sup.checked_in[i]) {
            /* At least one task missed its deadline — don't feed */
            return;
        }
    }
    /* All tasks healthy — feed the hardware WDT */
    hw_wdt_feed();

    /* Clear flags for the next cycle */
    for (uint8_t i = 0; i < wdt_sup.num_tasks; i++) {
        wdt_sup.checked_in[i] = false;
    }
}

How to wire it up:

  1. At init, each task calls wdt_register_task() and stores its ID.
  2. Each task calls wdt_task_checkin(my_id) after completing its main work each cycle, not at the top of the loop, but after the meaningful work is done.
  3. A timer fires at a regular interval (e.g., every 500ms) and calls wdt_supervisor_evaluate().
  4. The hardware WDT timeout is set to be longer than the supervisor period plus margin. If the supervisor runs every 500ms, a 1.5–2 second HW WDT timeout is reasonable.

Why this works: If any single task hangs, it stops checking in. The supervisor sees the missing flag. The hardware WDT starves. The system resets. No task can keep the watchdog alive on behalf of a dead peer.

Sizing the timeout: Your HW WDT timeout must be greater than your supervisor check period, which must be greater than your slowest task’s expected cycle time. If your slowest task runs at 200ms and your supervisor checks every 500ms, a 2-second HW WDT gives you a missed-supervisor-cycle plus margin.

Zephyr RTOS note: Zephyr provides a built-in task_wdt subsystem that implements exactly this pattern. Call task_wdt_init() to set up the supervisor, task_wdt_add() to register each task with its individual timeout, and task_wdt_feed() from within each task’s work loop. The subsystem handles supervisor evaluation and hardware WDT feeding internally. If you’re already on Zephyr, use this instead of rolling your own. It’s well-tested and integrates with Zephyr’s logging and reset-reason infrastructure. See the Zephyr Task Watchdog documentation for the full API reference.

Pattern 2: Windowed Watchdog for Loop-Timing Validation

The check-in pattern catches tasks that stop running. But what about tasks that run too fast?

Consider a sensor-reading task that normally blocks for 10ms waiting on an I2C transfer. If the I2C peripheral enters an error state, the read might return immediately with an error code that the task doesn’t handle correctly. The task then spins through its loop thousands of times per second, checking in enthusiastically every iteration. The supervisor sees rapid check-ins and happily feeds the WDT. Meanwhile, the system is reading garbage.

A standard watchdog can’t detect this. The feed is arriving more often, not less. A windowed watchdog enforces a minimum time between feeds. Feed too early and it triggers a reset, same as feeding too late.

Some MCUs (like STM32’s WWDG peripheral) support this in hardware. On platforms without hardware window support, you can implement a software window check:

#include <stdint.h>

static uint32_t last_feed_time = 0;
#define WDT_WINDOW_MIN_MS  50   /* Feed no sooner than 50ms */
#define WDT_WINDOW_MAX_MS  500  /* Feed no later than 500ms */

void wdt_windowed_feed(void) {
    uint32_t now = system_time_ms();
    uint32_t elapsed = now - last_feed_time;

    if (elapsed < WDT_WINDOW_MIN_MS) {
        /* Too early — system is running faster than expected.
           Let the HW WDT expire to force a reset. */
        return;
    }

    /* Within the valid window — feed the hardware WDT */
    hw_wdt_feed();
    last_feed_time = now;
}

If the elapsed time is under 50ms, the feed is suppressed. The hardware WDT will eventually expire and force a reset. You can combine this with Pattern 1: the supervisor calls wdt_windowed_feed() instead of hw_wdt_feed() directly, giving you both coverage against tasks that stop running and tasks that run too fast.

Pattern 3: Sequence Validation With a Token

For systems where execution order matters, not just “is each task alive?” but “are tasks executing in the correct sequence?”, a token-based watchdog adds another layer.

Concept: The supervisor issues a token value (e.g., a counter or nonce). Task A receives it, does its work, and passes it to Task B. Task B passes it to Task C. When Task C returns the token to the supervisor with the expected value, the supervisor feeds the WDT.

Picture a circular flow: Supervisor → Task A → Task B → Task C → Supervisor → WDT feed.

This validates the entire pipeline. If Task B skips, or tasks execute out of order, the token doesn’t complete the circuit, and the watchdog starves. This pattern is most valuable in pipeline architectures (sensor acquisition → signal processing → transmission) where out-of-order or skipped execution produces dangerous outputs. It’s more complex to implement than check-in, so reach for it when execution order is a safety or correctness requirement, not as a default.

What Happens After the Reset Fires

A watchdog reset without diagnostics is just a mystery reboot. Make resets useful.

Persist the reset reason. Most MCUs have a reset-cause register (e.g., RCC_CSR on STM32) that distinguishes watchdog resets from power-on, brown-out, or software resets. Read this register early in your boot sequence and log it to flash, to retained RAM, or over your telemetry channel. If you’re doing field failure analysis, this data is essential.

Implement a boot counter with fallback. Store a counter in non-volatile memory that increments on each watchdog reset and clears on a successful, sustained boot. If the counter exceeds a threshold (say, 3 consecutive WDT resets), enter a safe mode with reduced functionality, minimal peripherals, and just enough capability to accept a firmware update. Without this, a bug that triggers immediately after boot creates an infinite reset loop, and your device becomes a very expensive paperweight.

Debugging with watchdogs enabled. Most JTAG/SWD debuggers halt the WDT when you pause the core. But not always, and not on every MCU. Use a compile-time flag (#ifdef DEBUG_BUILD) to disable the WDT during development. Print a loud startup message — "WARNING: WDT DISABLED - DEBUG BUILD" — so this never silently ships to production.

Test the failure path, not just the happy path. Inject faults deliberately. Block a task with an infinite loop. Spin a task faster than expected. Verify the watchdog fires within your expected window. If you’ve never seen your watchdog actually reset the system during development, you don’t know if it works.

Building Your Watchdog Strategy This Week

A watchdog without a feeding strategy is security theater. It creates the appearance of reliability without the substance. Here’s what to do:

If you have the unconditional-feed anti-pattern today, start by moving to Pattern 1. Register every long-running task or subsystem with a supervisor. This single change catches the most common field failure: one subsystem hangs while the rest keep the watchdog alive.

If you have tight-loop failure modes (sensor reads, communication polls, anything that can return early on error), add windowed feeding on top of your supervisor.

If execution order is a safety requirement, evaluate whether token-based validation is worth the added complexity for your architecture.

Watchdogs handle one failure class: software hangs and deadlocks. They don’t catch corrupted data, memory degradation, or communication failures. A solid field reliability strategy layers watchdogs alongside safe boot sequences, memory integrity checks, and communications heartbeats. But the watchdog is the foundation. Get it right first.

Audit your current project against these patterns. Find the wdt_feed() call. Trace backward: what conditions must be true for that feed to execute? If the answer is “the main loop is running,” you have work to do.


Hubble Network connects your devices from anywhere on Earth via Bluetooth to satellite — so your watchdog data actually reaches you when field reliability matters most. See how it works →