Skip to content

The Ansible Handler Trap

When converting manual shell scripts into declarative Ansible playbooks, one of the most frustrating early pitfalls is the "Handler Trap".

It manifests as a playbook running successfully and reporting ok for all tasks, but the service you just configured doesn't restart, or the new settings never take effect.

How Handlers Work

In Ansible, a handler is a special task that is only triggered when explicitly notified by another task. They are designed to restart services or reload configurations only when something actually changes.

  tasks:
    - name: Update configuration file
      ansible.builtin.template:
        src: config.j2
        dest: /etc/myapp/config.conf
      notify: Restart MyApp

  handlers:
    - name: Restart MyApp
      ansible.builtin.systemd:
        name: myapp
        state: restarted

In the example above, Restart MyApp will only run if the template task actually modified the file on the remote server.

The Trap: Re-running Playbooks

If a playbook fails halfway through its execution (for example, due to a missing sudo password or a network timeout), the earlier tasks might have successfully changed state and notified their handlers. However, because the playbook crashed before reaching the end, the handlers never executed.

When you fix the error and run the playbook a second time, Ansible evaluates the first task and sees that /etc/myapp/config.conf is already correct. It reports ok (instead of changed). Because the state didn't change during this specific run, the handler is never notified.

Your configuration is written to disk, but the service is still running with the old configuration in memory!

How to Escape the Trap

There are three ways to solve this when developing playbooks or recovering from a crash:

1. Force Handlers

If a playbook fails mid-execution, you can force Ansible to run any notified handlers immediately, regardless of failures, by running the playbook with the --force-handlers flag.

2. Ad-hoc Recovery

If you've already re-run the playbook and the state is now ok, use an ad-hoc Ansible command to manually fire the handler command across the target nodes:

ansible webservers -m systemd -a "name=myapp state=restarted" -b -K

3. Override Changed Status

If you use a module like ansible.builtin.command or ansible.builtin.shell, Ansible cannot know if the command actually changed anything, so it always registers as changed. Conversely, if you capture the output of a command and intentionally suppress the change status (changed_when: false), you might inadvertently prevent a handler from ever firing.

Always explicitly define when a command constitutes a change:

    - name: Configure containerd for NVIDIA runtime
      ansible.builtin.command: nvidia-ctk runtime configure --runtime=containerd --set-as-default
      register: ctk_config
      # This ensures the handler fires if the command succeeded, 
      # but it's prone to the Trap on subsequent runs!
      changed_when: ctk_config.rc == 0 
      notify: Restart containerd