Skip to content

Introduction to Handlers


Handlers let you trigger specific tasks in response to changes made by other tasks. They’re useful when a configuration change needs a follow-up action, such as restarting a service, reloading a daemon, or applying a change that shouldn’t happen unless something actually changed. This is better built in design than is changed approach.

Defining A Handler

Handlers live in a play’s handlers: section, written with the exact same syntax as any other task:

handlers_demo.yaml
- name: Handlers Demo
  hosts: servers
  tasks:
    - name: Deploy configuration
      ansible.builtin.copy:
        src: app.conf
        dest: /etc/app.conf
      notify: Restart app

  handlers:
    - name: Restart app
      ansible.builtin.debug:
        msg: "Restarting the app now"

notify: — Triggering A Handler

notify: names the handler to trigger — the string has to match the handler’s name: exactly. When the notifying task reports changed, that handler gets queued to run — not immediately, but deferred, as covered shortly.

Rule One: Handlers Only Run If The Notifying Task Changed

This is the payoff. Run the playbook above once, against a config file that doesn’t exist yet:

changed: [ubuntu]
ok: [ubuntu] => { "msg": "Restarting the app now" }

Run it again, completely unchanged:

ok: [ubuntu]

No handler output at all, the second time — copy correctly reported no change, so Restart app was never even queued. This is exactly the manual register + is changed pattern from the tests and conditionals section, now automatic: the handler mechanism itself only fires on genuine change, with nothing extra to write.

Rule Two: A Handler Runs Only Once, No Matter How Many Times It’s Notified

Multiple tasks can notify the same handler — and it still runs exactly once:

tasks:
  - name: Deploy config A
    ansible.builtin.copy:
      src: a.conf
      dest: /etc/a.conf
    notify: Restart app

  - name: Deploy config B
    ansible.builtin.copy:
      src: b.conf
      dest: /etc/b.conf
    notify: Restart app

handlers:
  - name: Restart app
    ansible.builtin.debug:
      msg: "Restarting the app now"

If both config files are new, both tasks report changed, and both notify Restart app — but the output still shows the handler firing only once, not twice. Ansible automatically deduplicates repeated notifications to the same handler within a single play run.

When Do Handlers Actually Run?

By default, handlers run at the end of the play, after every regular task has finished — not immediately after whichever task notified them. A task near the top of your task list and one near the bottom can both notify the same handler, and it still only runs once, at the very end. (There’s a way to change this timing, covered in the next chapter.)

Handlers And Loops: A Real Trip-Up

Here’s the assumption almost everyone makes, and it’s wrong: “if 3 out of 5 loop iterations changed something, the handler must run 3 times.” It doesn’t. It runs once, exactly the same as Rule Two above — a looped task is still one task, and however many of its iterations report changed, the handler it notifies fires only a single time.

looped_notify.yaml
- name: Create multiple config files
  ansible.builtin.copy:
    content: "config for {{ item }}"
    dest: "/etc/{{ item }}.conf"
  loop:
    - app1
    - app2
    - app3
  notify: Restart app

handlers:
  - name: Restart app
    ansible.builtin.debug:
      msg: "Restarting the app now"

Suppose app1.conf and app2.conf are new, and app3.conf already exists with identical content — two changed iterations, one unchanged. Restart app is still triggered by the loop overall (since some iteration changed), and it still only runs once at the end of the play, not twice for the two iterations that changed and not three times for every item in the loop. The mental model is exactly Rule Two: it doesn’t matter how a handler gets notified — one task, several tasks, or several iterations of one looped task — it collapses to a single execution regardless.

Best Practices

  • Rely on Rule One instead of manually building register + is changed logic — that’s precisely the problem handlers exist to solve automatically.
  • Notify freely from multiple related tasks without worrying about redundant execution — Rule Two’s deduplication handles that for you.
  • Never assume a looped task’s notify count scales with how many iterations changed — one loop, however many changed items, is still exactly one notification worth of effect.
Last updated on